Skip to content

File burst_despreader_core.h

FileList > burst_despreader > burst_despreader_core.h

Go to the source code of this file

BurstDespreader component API. More...

  • #include "clib_common.h"
  • #include "dp_state.h"
  • #include "jm_perf.h"
  • #include "loop_filter/loop_filter_core.h"

Classes

Type Name
struct burst_despreader_state_t
BurstDespreader state.

Public Functions

Type Name
size_t burst_despreader_bits (burst_despreader_state_t * state, const float _Complex * x, size_t x_len, uint8_t * out, size_t max_out)
Despread a CF32 block; emit one hard BPSK bit (0/1) per code period.
size_t burst_despreader_bits_max_out (burst_despreader_state_t * state)
Upper bound on bits burst_despreader_bits can emit (0; see burst_despreader_steps_max_out).
burst_despreader_state_t * burst_despreader_create (const uint8_t * code, size_t code_len, size_t sf, size_t sps, double init_norm_freq, double init_chip_phase, double bn_carrier, double bn_code)
Create a burst despreader instance.
void burst_despreader_destroy (burst_despreader_state_t * state)
Destroy a burst despreader instance and release all memory.
double burst_despreader_get_bn_carrier (const burst_despreader_state_t * state)
Carrier (Costas) loop noise bandwidth, normalized to the symbol rate.
double burst_despreader_get_bn_code (const burst_despreader_state_t * state)
Code (DLL) loop noise bandwidth, normalized to the symbol rate.
double burst_despreader_get_code_phase (const burst_despreader_state_t * state)
Current tracked code phase within the symbol, chips.
double burst_despreader_get_lock_metric (const burst_despreader_state_t * state)
Lock indicator in [0,1] : the mean of |Re prompt|/|prompt| over every prompt of the burst (cumulative, not EMA — a one-shot burst gives each prompt equal weight instead of spending the whole burst warming a smoother up). ~1 when phase-locked; ~2/pi (0.637) with no carrier (|cos theta|, uniform theta).
double burst_despreader_get_lock_stat (const burst_despreader_state_t * state)
Calibrated whole-burst lock statistic (the one-shot analog of the tracking loops' verify-counted detectors).
double burst_despreader_get_norm_freq (const burst_despreader_state_t * state)
Current carrier frequency estimate, cycles/sample.
double burst_despreader_get_snr_est (const burst_despreader_state_t * state)
Post-despread SNR estimate over the burst, accumulate-then-ratio: (sum Re^2 - sum Im^2) / sum Im^2, clamped >= 0. For BPSK the signal lives in Re and the noise splits evenly, so this estimates A^2/sigma^2 (per-component) directly — unlike a per-symbol Re^2/Im^2 ratio, whose heavy-tailed reciprocal chi-square makes the estimate biased high with enormous variance. This is the EFFECTIVE post-loop SNR: residual tracking-loop phase jitter rotates signal energy into Im, so the estimate sits below the AWGN-only value by the jitter term (converging as bn -> 0) — the quantity that actually predicts demodulation performance.
size_t burst_despreader_get_stat_n (const burst_despreader_state_t * state)
Number of prompts folded into the burst statistics so far.
void burst_despreader_get_state (const burst_despreader_state_t * state, void * blob)
void burst_despreader_reset (burst_despreader_state_t * state)
Re-seed the loops to the create-time phase/frequency and re-arm the burst statistics; preserve config.
void burst_despreader_set_acq (burst_despreader_state_t * state, const uint8_t * acq_code, size_t acq_code_len, size_t acq_reps)
Enable preamble-aided pull-in with a distinct acquisition code.
void burst_despreader_set_bn_carrier (burst_despreader_state_t * state, double val)
Set the carrier loop bandwidth (recomputes the loop gains).
void burst_despreader_set_bn_code (burst_despreader_state_t * state, double val)
Set the code loop bandwidth (recomputes the loop gains).
void burst_despreader_set_norm_freq (burst_despreader_state_t * state, double val)
Override the carrier frequency estimate, cycles/sample (re-seed).
int burst_despreader_set_state (burst_despreader_state_t * state, const void * blob)
size_t burst_despreader_state_bytes (const burst_despreader_state_t * state)
size_t burst_despreader_steps (burst_despreader_state_t * state, const float _Complex * x, size_t x_len, float _Complex * out, size_t max_out)
Despread a CF32 block; emit one complex prompt symbol per code period.
size_t burst_despreader_steps_max_out (burst_despreader_state_t * state)
Upper bound on symbols burst_despreader_steps can emit (0; the caller sizes the output buffer to the input length, which always suffices).

Macros

Type Name
define BURST_DESPREADER_STATE_MAGIC [**DP\_FOURCC**](dp__state_8h.md#define-dp_fourcc) ('B','D','S','P')
define BURST_DESPREADER_STATE_VERSION 2u /\* v2: cumulative burst statistics \*/

Detailed Description

Lifecycle: create -> (steps / bits / reset)* -> destroy

This object despreads a BLOCK: one prompt sample per spread symbol, so there is no scalar step a single input chip is not a symbol.

Example:

static const uint8_t code[4] = { 1, 0, 1, 1 };
burst_despreader_state_t *obj
    = burst_despreader_create (code, 4, 4, 2, 0.0, 0.0, 0.05, 0.01);
float _Complex in[8]  = { 0 };
float _Complex out[8] = { 0 };
size_t n = burst_despreader_steps (obj, in, 8, out,
                                   burst_despreader_steps_max_out (obj));
burst_despreader_destroy (obj);

Public Functions Documentation

function burst_despreader_bits

Despread a CF32 block; emit one hard BPSK bit (0/1) per code period.

size_t burst_despreader_bits (
    burst_despreader_state_t * state,
    const float _Complex * x,
    size_t x_len,
    uint8_t * out,
    size_t max_out
) 

Same streaming kernel as burst_despreader_steps(), but emits the hard decision crealf(prompt) >= 0 instead of the complex symbol.

Parameters:

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

Returns:

Number of hard bits written into out.

>>> import numpy as np
>>> from doppler.dsss import BurstDespreader
>>> rng = np.random.default_rng(1)
>>> code = rng.integers(0, 2, 31).astype(np.uint8)
>>> bits = rng.integers(0, 2, 30).astype(np.uint8)
>>> chips = np.where(code & 1, -1.0, 1.0)
>>> syms = np.where(bits == 1, -1.0, 1.0)
>>> tx = np.concatenate(
...     [np.repeat(s * chips, 4) for s in syms]).astype(np.complex64)
>>> d = BurstDespreader(code, sf=31, sps=4)
>>> rec = d.bits(tx)                             # hard 0/1 per symbol
>>> rec.shape
(30,)
>>> e = np.mean(rec != bits)             # up to a BPSK sign flip
>>> round(float(min(e, 1.0 - e)), 4)
0.0
>>> round(d.lock_metric, 3)
1.0


function burst_despreader_bits_max_out

Upper bound on bits burst_despreader_bits can emit (0; see burst_despreader_steps_max_out).

size_t burst_despreader_bits_max_out (
    burst_despreader_state_t * state
) 


function burst_despreader_create

Create a burst despreader instance.

burst_despreader_state_t * burst_despreader_create (
    const uint8_t * code,
    size_t code_len,
    size_t sf,
    size_t sps,
    double init_norm_freq,
    double init_chip_phase,
    double bn_carrier,
    double bn_code
) 

Parameters:

  • code Data spreading code (0/1 chips), length code_len; copied.
  • code_len Length of code in chips (>= sf).
  • sf Spreading factor: chips integrated per prompt symbol (default: 1).
  • sps Samples per chip (default: 2).
  • init_norm_freq Seed carrier frequency, cycles/sample — the acquisition estimate (default: 0.0).
  • init_chip_phase Seed code phase, chips (default: 0.0).
  • bn_carrier Carrier (Costas) loop noise bandwidth, normalized to the symbol rate (default: 0.05).
  • bn_code Code (DLL) loop noise bandwidth, normalized to the symbol rate (default: 0.01).

Returns:

Heap-allocated state, or NULL on allocation failure.

Note:

Caller must call burst_despreader_destroy() when done.

>>> import numpy as np
>>> from doppler.dsss import BurstDespreader
>>> rng = np.random.default_rng(1)
>>> code = rng.integers(0, 2, 31).astype(np.uint8)  # length-31 code
>>> chips = np.where(code & 1, -1.0, 1.0)    # 0 -> +1, 1 -> -1
>>> bits = rng.integers(0, 2, 30).astype(np.uint8)    # payload bits
>>> syms = np.where(bits == 1, -1.0, 1.0)             # BPSK symbols
>>> tx = np.concatenate(
...     [np.repeat(s * chips, 4) for s in syms]).astype(np.complex64)
>>> b = BurstDespreader(code, sf=31, sps=4)           # 31 chips/symbol
>>> sym = b.steps(tx)                        # one prompt/symbol
>>> sym.shape
(30,)
>>> hard = (sym.real < 0).astype(np.uint8)            # BPSK decision
>>> float(np.mean(hard != bits))             # payload recovered
0.0


function burst_despreader_destroy

Destroy a burst despreader instance and release all memory.

void burst_despreader_destroy (
    burst_despreader_state_t * state
) 

Parameters:

  • state May be NULL.

function burst_despreader_get_bn_carrier

Carrier (Costas) loop noise bandwidth, normalized to the symbol rate.

double burst_despreader_get_bn_carrier (
    const burst_despreader_state_t * state
) 


function burst_despreader_get_bn_code

Code (DLL) loop noise bandwidth, normalized to the symbol rate.

double burst_despreader_get_bn_code (
    const burst_despreader_state_t * state
) 


function burst_despreader_get_code_phase

Current tracked code phase within the symbol, chips.

double burst_despreader_get_code_phase (
    const burst_despreader_state_t * state
) 


function burst_despreader_get_lock_metric

Lock indicator in [0,1] : the mean of |Re prompt|/|prompt| over every prompt of the burst (cumulative, not EMA — a one-shot burst gives each prompt equal weight instead of spending the whole burst warming a smoother up). ~1 when phase-locked; ~2/pi (0.637) with no carrier (|cos theta|, uniform theta).

double burst_despreader_get_lock_metric (
    const burst_despreader_state_t * state
) 


function burst_despreader_get_lock_stat

Calibrated whole-burst lock statistic (the one-shot analog of the tracking loops' verify-counted detectors).

double burst_despreader_get_lock_stat (
    const burst_despreader_state_t * state
) 

R = sqrt(stat_n * sum Re^2 / sum Im^2): the burst's coherent (in-phase) energy normalised by the noise power estimated from the quadrature arm. Because the noise reference is estimated from the SAME number of samples as the signal sum, the exact H0 law is R^2 = stat_n * F(stat_n, stat_n) — NOT chi-square (a chi-square gate assumes a known noise power and realizes tens of times the priced pfa here). The closed-form gate is

locked_burst = R > sqrt(stat_n * det_threshold_f(pfa, stat_n))

exact for every stat_n (odd included). Only payload prompts fold into the statistics — preamble prompts (different code length, pull-in transients) are excluded so the H0 law and the SNR calibration hold.

RETURNS 0 IN TWO CASES, and a caller must separate them with stat_n: before any payload prompt has been folded (stat_n == 0), and when the quadrature sum is exactly zero (stat_n > 0), where the ratio is undefined and there is no statistic to report. The second only happens on a perfectly noiseless input — which never occurs on the air and occurs constantly in tests, so it is worth knowing that the WORST possible reading of this statistic is what a synthetic clean burst produces. The example below therefore carries a noise floor, which is also the only condition under which a lock statistic means anything.

>>> import numpy as np
>>> from doppler.dsss import BurstDespreader
>>> from doppler.detection import det_threshold_f
>>> code = (np.arange(31) % 2).astype(np.uint8)
>>> chips = 1.0 - 2.0 * (code % 2)
>>> clean = np.tile(np.repeat(chips, 2), 64)
>>> rng = np.random.default_rng(7)
>>> n = (rng.standard_normal(clean.size)
...      + 1j * rng.standard_normal(clean.size)) / np.sqrt(2)
>>> b = BurstDespreader(code=code, sf=31, sps=2)
>>> _ = b.steps((clean + 0.3 * n).astype(np.complex64))
>>> eta = np.sqrt(b.stat_n * det_threshold_f(1e-3, b.stat_n))
>>> bool(b.lock_stat > eta)   # this burst passes the pfa=1e-3 gate
True

function burst_despreader_get_norm_freq

Current carrier frequency estimate, cycles/sample.

double burst_despreader_get_norm_freq (
    const burst_despreader_state_t * state
) 


function burst_despreader_get_snr_est

Post-despread SNR estimate over the burst, accumulate-then-ratio: (sum Re^2 - sum Im^2) / sum Im^2, clamped >= 0. For BPSK the signal lives in Re and the noise splits evenly, so this estimates A^2/sigma^2 (per-component) directly — unlike a per-symbol Re^2/Im^2 ratio, whose heavy-tailed reciprocal chi-square makes the estimate biased high with enormous variance. This is the EFFECTIVE post-loop SNR: residual tracking-loop phase jitter rotates signal energy into Im, so the estimate sits below the AWGN-only value by the jitter term (converging as bn -> 0) — the quantity that actually predicts demodulation performance.

double burst_despreader_get_snr_est (
    const burst_despreader_state_t * state
) 


function burst_despreader_get_stat_n

Number of prompts folded into the burst statistics so far.

size_t burst_despreader_get_stat_n (
    const burst_despreader_state_t * state
) 


function burst_despreader_get_state

void burst_despreader_get_state (
    const burst_despreader_state_t * state,
    void * blob
) 

function burst_despreader_reset

Re-seed the loops to the create-time phase/frequency and re-arm the burst statistics; preserve config.

void burst_despreader_reset (
    burst_despreader_state_t * state
) 

Restores the carrier NCO to the seed frequency and the code phase to the seed chip, zeroes the loop accumulators, and clears the cumulative burst read-backs (lock_metric / snr_est / lock_stat / stat_n) — the spreading code and bandwidths are kept. Call it between bursts so each burst's statistics start clean; a prior burst_despreader_set_acq() preamble is also re-armed.

Parameters:

  • state Must be non-NULL.
    >>> import numpy as np
    >>> from doppler.dsss import BurstDespreader
    >>> rng = np.random.default_rng(1)
    >>> 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, 30) == 1, -1.0, 1.0)
    >>> tx = np.concatenate(
    ...     [np.repeat(s * chips, 4) for s in syms]).astype(np.complex64)
    >>> d = BurstDespreader(code, sf=31, sps=4)
    >>> first = d.bits(tx)
    >>> d.reset()                          # re-arm for a new burst
    >>> np.array_equal(first, d.bits(tx))  # same as a fresh object
    True
    

function burst_despreader_set_acq

Enable preamble-aided pull-in with a distinct acquisition code.

void burst_despreader_set_acq (
    burst_despreader_state_t * state,
    const uint8_t * acq_code,
    size_t acq_code_len,
    size_t acq_reps
) 

Track acq_reps periods of acq_code coherently (the unmodulated, repeated acquisition preamble — a full ±pi phase discriminator, so the loops pull in even a wide residual) before switching to the data code for the payload. Call before feeding the burst; the acq mode clears automatically once the preamble is consumed, and re-arms on burst_despreader_reset(). NB: set_acq re-arms the PREAMBLE only — the cumulative burst statistics (lock_metric / snr_est / lock_stat / stat_n) are re-armed by burst_despreader_reset(); call it between bursts.

Parameters:

  • state Must be non-NULL.
  • acq_code Acquisition code (0/1), length acq_code_len; copied.
  • acq_code_len Acquisition code length in chips.
  • acq_reps Number of acq-code periods in the preamble.
    >>> import numpy as np
    >>> from doppler.dsss import BurstDespreader
    >>> rng = np.random.default_rng(5)
    >>> acq = rng.integers(0, 2, 128).astype(np.uint8)    # long acq code
    >>> data_code = rng.integers(0, 2, 32).astype(np.uint8)
    >>> pbits = rng.integers(0, 2, 40).astype(np.uint8)
    >>> asig = np.where(acq & 1, -1.0, 1.0)
    >>> dch = np.where(data_code & 1, -1.0, 1.0)
    >>> psyms = np.where(pbits == 1, -1.0, 1.0)
    >>> pre = np.concatenate([np.repeat(asig, 4) for _ in range(4)])
    >>> pay = np.concatenate([np.repeat(s * dch, 4) for s in psyms])
    >>> burst = np.concatenate([pre, pay]).astype(np.complex64)
    >>> d = BurstDespreader(data_code, sf=32, sps=4)
    >>> d.set_acq(acq, 4)            # 4 preamble reps, pulls loops in
    >>> out = d.bits(burst)          # preamble emits nothing
    >>> out.shape                    # only the payload symbols come out
    (40,)
    >>> e = np.mean(out != pbits)
    >>> round(float(min(e, 1.0 - e)), 4)
    0.0
    

function burst_despreader_set_bn_carrier

Set the carrier loop bandwidth (recomputes the loop gains).

void burst_despreader_set_bn_carrier (
    burst_despreader_state_t * state,
    double val
) 


function burst_despreader_set_bn_code

Set the code loop bandwidth (recomputes the loop gains).

void burst_despreader_set_bn_code (
    burst_despreader_state_t * state,
    double val
) 


function burst_despreader_set_norm_freq

Override the carrier frequency estimate, cycles/sample (re-seed).

void burst_despreader_set_norm_freq (
    burst_despreader_state_t * state,
    double val
) 


function burst_despreader_set_state

int burst_despreader_set_state (
    burst_despreader_state_t * state,
    const void * blob
) 

function burst_despreader_state_bytes

size_t burst_despreader_state_bytes (
    const burst_despreader_state_t * state
) 

function burst_despreader_steps

Despread a CF32 block; emit one complex prompt symbol per code period.

size_t burst_despreader_steps (
    burst_despreader_state_t * state,
    const float _Complex * x,
    size_t x_len,
    float _Complex * out,
    size_t max_out
) 

Streams: a partial symbol is carried in state across calls. Each emitted symbol is the complex prompt integrate-and-dump (carrier-wiped, code-stripped) — its sign is the BPSK decision, its phase/magnitude the soft information. During a burst_despreader_set_acq preamble no symbols are emitted (the loops are pulling in); payload symbols follow.

Parameters:

  • state Must be non-NULL.
  • x Input CF32 samples, length x_len.
  • 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 BurstDespreader
>>> rng = np.random.default_rng(1)
>>> code = rng.integers(0, 2, 31).astype(np.uint8)  # length-31 code
>>> bits = rng.integers(0, 2, 30).astype(np.uint8)   # payload bits
>>> chips = np.where(code & 1, -1.0, 1.0)    # 0 -> +1, 1 -> -1
>>> syms = np.where(bits == 1, -1.0, 1.0)             # BPSK symbols
>>> tx = np.concatenate(
...     [np.repeat(s * chips, 4) for s in syms]).astype(np.complex64)
>>> d = BurstDespreader(code, sf=31, sps=4)
>>> sym = d.steps(tx)                        # one prompt/symbol
>>> sym.shape
(30,)
>>> hard = (sym.real < 0).astype(np.uint8)            # BPSK decision
>>> float(np.mean(hard != bits))             # payload recovered
0.0

function burst_despreader_steps_max_out

Upper bound on symbols burst_despreader_steps can emit (0; the caller sizes the output buffer to the input length, which always suffices).

size_t burst_despreader_steps_max_out (
    burst_despreader_state_t * state
) 


Macro Definition Documentation

define BURST_DESPREADER_STATE_MAGIC

#define BURST_DESPREADER_STATE_MAGIC `DP_FOURCC ('B','D','S','P')`

define BURST_DESPREADER_STATE_VERSION

#define BURST_DESPREADER_STATE_VERSION `2u /* v2: cumulative burst statistics */`


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