File async_dsss_receiver_core.h¶
FileList > async_dsss_receiver > async_dsss_receiver_core.h
Go to the source code of this file
Composed continuous DSSS receiver: Acquisition -> hand-off -> CarrierAcquisition refine -> Costas/Dll/RateConverter/ MpskReceiver track, one object. More...
#include "doppler/RateConverter/RateConverter_core.h"#include "doppler/acq/acq_core.h"#include "doppler/carrier_acq/carrier_acq_core.h"#include "doppler/cic/cic_core.h"#include "doppler/costas/costas_core.h"#include "doppler/dll/dll_core.h"#include "doppler/dp_state.h"#include "doppler/hbdecim/hbdecim_core.h"#include "doppler/lockdet/lockdet_core.h"#include "doppler/mpsk_receiver/mpsk_receiver_core.h"#include "doppler/resamp/resamp_core.h"#include "doppler/resample/resample_core.h"#include "doppler/dp_complex.h"#include <stdbool.h>#include <stddef.h>#include "doppler/psd/psd_core.h"#include "doppler/detector/detector_core.h"#include "doppler/detection/detection_core.h"#include "doppler/spectral/spectral_core.h"#include "doppler/corr/corr_core.h"#include "doppler/fft/fft_core.h"#include "doppler/acc_trace/acc_trace_core.h"#include "doppler/ber/ber_core.h"#include "doppler/doppler_channel/doppler_channel_core.h"#include "doppler/dp_tlm/dp_tlm_core.h"
Classes¶
| Type | Name |
|---|---|
| struct | async_dsss_receiver_extra_t |
| struct | async_dsss_receiver_status_t One consistent picture of what the receiver is doing, by value. |
| struct | dp_async_dsss_receiver_state_t Composed receiver state. |
Public Types¶
| Type | Name |
|---|---|
| enum | async_dsss_rx_state_t |
Public Functions¶
| Type | Name |
|---|---|
| int | dp_async_dsss_receiver_configure_chain_raw (dp_async_dsss_receiver_state_t * state, size_t segments, size_t sps, int n) Pin the live-tracking despread/resample/demod grid directly. The escape hatch for the composition-specific knob: segments (the live Dll's tracking parameter) andsps /n (MpskReceiver's samples and terminal outputs per symbol) are independently overridable, still bridged by a freshly-sizedRateConverter and never coupled to each other. While searching/refining it re-pins the grid used to build the next tracking chain; once tracking it rebuildsdll /rc /rx in place, allocating every replacement before adopting it, and re-seeds the rebuilt chain from the whole carrier loop 1 plus loop 2's integrator, the sum status() reports so a retune keeps the signal it was tracking. |
| void | dp_async_dsss_receiver_configure_lock_raw (dp_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) Re-tune the live-tracking Dll's code-lock detector directly. Forwards to dp_dll_configure_lock_raw() on the live tracking Dll (the one behindget_code_locked() ), NOT the refine-stage collection Dll. It retunes the Dll the receiver holds now; every chain build (a seed, the hand-over, reset() or configure_chain_raw()) replaces that Dll with a freshly sized one, so call it once tracking. The detector is the hysteretic lockdet over the DLL's per-N-look CFAR statistic — the levels and verify counts trade declare latency against false-alarm rate. |
| int | dp_async_dsss_receiver_configure_search_raw (dp_async_dsss_receiver_state_t * state, size_t doppler_bins, size_t n_noncoh) Pin the embedded Acquisition's search grid directly. Forwards to dp_acq_configure_search_raw() __ the escape hatch under this object'ssymbol_rate -driven auto-sizing, for a power user who wants a specific(doppler_bins, n_noncoh) . The pin replaces the whole auto-sized grid, the window-tiled search included. Only meaningful while searching; the search does not run again until the nextreset() . |
| dp_async_dsss_receiver_state_t * | dp_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, bool reacquire, double code_only_s, double doppler_rate) Create an AsyncDsssReceiver in the searching state. |
| void | dp_async_dsss_receiver_destroy (dp_async_dsss_receiver_state_t * state) Destroy a receiver and release every child. |
| double | dp_async_dsss_receiver_get_car_last_error (const dp_async_dsss_receiver_state_t * state) Pre-despread Costas phase discriminator (rad): the residual carrier phase LOOP 1 (which de-rotates before the Dll) is not nulling. |
| double | dp_async_dsss_receiver_get_car_nco_freq (const dp_async_dsss_receiver_state_t * state) LOOP 1 (pre-despread Costas) loop-filter output = NCO frequency command, cycles/sample of the front-end (chip_rate*spc) rate. |
| double | dp_async_dsss_receiver_get_chip_phase (const dp_async_dsss_receiver_state_t * state) The live Dll's code phase, chips, in the Dll's instantaneous convention the mirror ofacq_result_t::code_phase 's correlation-lag convention (dp_acq_build_handoff() ). While refining there is no live Dll: it is the seed's phase advanced by the clock dilation since, the phase the hand-over starts the live chain from. |
| double | dp_async_dsss_receiver_get_cn0_dbhz_est (const dp_async_dsss_receiver_state_t * state) The seed's C/N0 estimate, dB-Hz, unchanged until reset(). |
| int | dp_async_dsss_receiver_get_code_locked (const dp_async_dsss_receiver_state_t * state) _Binary code-lock flag from the live tracking Dll's own verify-counted (pfa-tuned) lock detector — the fundamental DSSS "am I |
| despreading" lock, de-chattered by up/down hysteresis._ | |
| double | dp_async_dsss_receiver_get_code_rate (const dp_async_dsss_receiver_state_t * state) The Dll's code rate, chips per nominal chip (~1.0 at lock), without the carrier aid; a coasting Dll reports its held rate. |
| size_t | dp_async_dsss_receiver_get_coherent_bins (const dp_async_dsss_receiver_state_t * state) The searcher's slow-time depth D: epochs per coherent block. |
| double | dp_async_dsss_receiver_get_doppler_hz (const dp_async_dsss_receiver_state_t * state) The Doppler estimate the chain was built from, Hz: the seed's while refining, the CarrierAcquisition-refined value once tracking. The live carrier is status().doppler_hz. |
| double | dp_async_dsss_receiver_get_lock (const dp_async_dsss_receiver_state_t * state) The carrier lock statistic of the MpskReceiver this receiver tracks with: the EMA of its M-th-power lock signal, near 1 when locked and near 0 on noise. |
| double | dp_async_dsss_receiver_get_lock_metric (const dp_async_dsss_receiver_state_t * state) Symbol-lock metric = SNR-weighted EMA of (I^2-Q^2)/(I^2+Q^2) = cos(2*phi) over the emitted symbols (locked -> ~+1). Drives locked . |
| double | dp_async_dsss_receiver_get_lock_threshold (const dp_async_dsss_receiver_state_t * state) The lock-metric threshold locked is declared strictly above (0.5, ASYNC_DSSS_RX_LOCK_UP); exposed alongside lock_metric for engineering debug. |
| int | dp_async_dsss_receiver_get_locked (const dp_async_dsss_receiver_state_t * state) The receiver's symbol-lock flag: a hysteretic detector over get_lock_metric(), declared after 30 consecutive symbols strictly above get_lock_threshold() (0.5) and dropped after 15 consecutive symbols strictly below 0.3 (ASYNC_DSSS_RX_LOCK_N_UP, _UP, _N_DOWN and _DOWN, fixed at create). The flag the release rule and a pool's holder act on. |
| int | dp_async_dsss_receiver_get_lost (const dp_async_dsss_receiver_state_t * state) 1 once the release rule has fired: both lock flags were down, continuously, for longer than lost_confirm_s while tracking an emitter that left, or a seed that never locked. The loops have stopped and samples are consumed and discarded; the holder releases the assignment and calls reset(). 0 in every other state, and always 0 withlost_confirm_s = 0 . |
| double | dp_async_dsss_receiver_get_mpsk_last_error (const dp_async_dsss_receiver_state_t * state) MpskReceiver carrier phase discriminator (rad): the residual carrier phase LOOP 2 (post-despread) is not nulling. |
| int | dp_async_dsss_receiver_get_n (const dp_async_dsss_receiver_state_t * state) |
| size_t | dp_async_dsss_receiver_get_n_exclusions (const dp_async_dsss_receiver_state_t * state) Code phases set_exclusions() holds. |
| double | dp_async_dsss_receiver_get_nco_freq (const dp_async_dsss_receiver_state_t * state) The MpskReceiver's carrier NCO command, its loop filter's whole output, cycles/sample at its input rate ( sps * symbol_rate ). Its mean tracks a Doppler ramp with no lag (unlike get_norm_freq()); its variance is the carrier loop's stress. |
| double | dp_async_dsss_receiver_get_norm_freq (const dp_async_dsss_receiver_state_t * state) The post-despread carrier the MpskReceiver tracks: its centre plus its loop's integrator, cycles/sample at its input rate ( sps * symbol_rate ). Lags a Doppler ramp by the Type-II ramp error. |
| size_t | dp_async_dsss_receiver_get_rank (const dp_async_dsss_receiver_state_t * state) Which surviving peak of a dwell the search takes; 0 = the strongest. Set with dp_async_dsss_receiver_set_rank() . |
| int | dp_async_dsss_receiver_get_refining (const dp_async_dsss_receiver_state_t * state) 1 while refining: the CarrierAcquisition dwell; 0 in every other state. |
| size_t | dp_async_dsss_receiver_get_segments (const dp_async_dsss_receiver_state_t * state) |
| size_t | dp_async_dsss_receiver_get_sps (const dp_async_dsss_receiver_state_t * state) |
| void | dp_async_dsss_receiver_get_state (const dp_async_dsss_receiver_state_t * state, void * blob) |
| int | dp_async_dsss_receiver_get_threads (const dp_async_dsss_receiver_state_t * state) Workers the searcher runs, the calling thread included; 1 = serial. Set with dp_async_dsss_receiver_set_threads() . |
| int | dp_async_dsss_receiver_get_tracking (const dp_async_dsss_receiver_state_t * state) 1 once the live tracking chain is built and demodulating; 0 in every other state. |
| void | dp_async_dsss_receiver_reset (dp_async_dsss_receiver_state_t * state) Return to the searching state. Resets the embedded Acquisition and rebuilds the refine-stage chain and the live tracking chain back to their placeholder seed (phase 0, no Doppler). A receiver that has locked cannot be "reset back to tracking the same signal," only back to searching every object's reset() semantics in this codebase. |
| int | dp_async_dsss_receiver_seed (dp_async_dsss_receiver_state_t * state, double chip_phase, double doppler_hz_est, double cn0_dbhz_est) Take a detection from outside and start refining from it. |
| int | dp_async_dsss_receiver_seed_ok (const dp_async_dsss_receiver_state_t * state, double chip_phase, double doppler_hz_est) The seed domain: whether (chip_phase, doppler_hz_est) is onedp_async_dsss_receiver_seed() accepts, and one a live receiver can hold. |
| int | dp_async_dsss_receiver_set_exclusions (dp_async_dsss_receiver_state_t * state, const double * phases, size_t phases_len) Code phases the search must not take: signals another receiver already tracks. |
| int | dp_async_dsss_receiver_set_rank (dp_async_dsss_receiver_state_t * state, size_t rank) Take the rank-th surviving peak of a dwell, not the strongest. |
| int | dp_async_dsss_receiver_set_refine_min_blocks (dp_async_dsss_receiver_state_t * state, size_t n_blocks) Floor the refine's dwell at n_blocks , whatever the detection sizing asks (docs/design/async-dsss-receiver-measurements.md §12.16, #1265). |
| int | dp_async_dsss_receiver_set_state (dp_async_dsss_receiver_state_t * state, const void * blob) |
| int | dp_async_dsss_receiver_set_telemetry (dp_async_dsss_receiver_state_t * state, dp_tlm_t * tlm, const char * prefix, uint32_t decim) Attach (or detach) a telemetry context across the receiver's loops, and register the receiver's own lifecycle and symbol-lock decision. |
| int | dp_async_dsss_receiver_set_threads (dp_async_dsss_receiver_state_t * state, int n) Set the workers the searcher fans its Doppler tiles across. |
| size_t | dp_async_dsss_receiver_state_bytes (const dp_async_dsss_receiver_state_t * state) |
| async_dsss_receiver_status_t | dp_async_dsss_receiver_status (const dp_async_dsss_receiver_state_t * state) Read the status record, by value: the state; where the emitter is now (the live carrier), its chip phase, code rate and C/N0; both lock flags with the symbol-lock metric and threshold; both carrier residuals; and the two clocks in input samples (since the state was entered; both flags down without a break). |
| size_t | dp_async_dsss_receiver_steps (dp_async_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 tracking. |
| size_t | dp_async_dsss_receiver_steps_max_out (dp_async_dsss_receiver_state_t * state) The output capacity steps() needs beyond the input's. |
Macros¶
| Type | Name |
|---|---|
| define | ASYNC_DSSS_RECEIVER_STATE_MAGIC [**DP\_FOURCC**](dp__state_8h.md#define-dp_fourcc) ('A', 'D', 'R', 'X') |
| define | ASYNC_DSSS_RECEIVER_STATE_VERSION 13u /\* v13: rank, exclusions; v12: samples\_in, the dwell alignment, threads; v11: no cell mode; v10: the dll children grew (output level, #2495); v9: refine\_min\_blocks; v8: refused\_hits; v7: no hand-off flavor; v6: the cell pull-in \*/ |
| define | ASYNC_DSSS_RX_BN_CARRIER 0.04 |
| define | ASYNC_DSSS_RX_CARRIER_PULLIN_HZ (chip_rate, code_len) ([**ASYNC\_DSSS\_RX\_BN\_CARRIER**](async__dsss__receiver__core_8h.md#define-async_dsss_rx_bn_carrier) \* (chip\_rate) / (2.0 \* (double)(code\_len))) |
| define | ASYNC_DSSS_RX_DEMOD_BN_CARRIER 0.01 |
| define | ASYNC_DSSS_RX_DEMOD_BN_TIMING 0.005 |
| define | ASYNC_DSSS_RX_DEMOD_LOCK_THRESH 0.3 |
| define | ASYNC_DSSS_RX_DEMOD_RRC_BETA 0.35 |
| define | ASYNC_DSSS_RX_DEMOD_RRC_SPAN 8 |
| define | ASYNC_DSSS_RX_DEMOD_ZETA 0.707 |
| define | ASYNC_DSSS_RX_DLL_BN 0.002 |
| define | ASYNC_DSSS_RX_DLL_SPACING 0.5 |
| define | ASYNC_DSSS_RX_DLL_ZETA 0.707 |
| define | ASYNC_DSSS_RX_LOCK_DOWN 0.3 |
| define | ASYNC_DSSS_RX_LOCK_DWELL 30u |
| define | ASYNC_DSSS_RX_LOCK_N_DOWN 15u |
| define | ASYNC_DSSS_RX_LOCK_N_UP 30u |
| define | ASYNC_DSSS_RX_LOCK_UP 0.5 |
| define | ASYNC_DSSS_RX_MAX_EXCLUSIONS 64u |
| define | ASYNC_DSSS_RX_MAX_PEAKS 16u |
| define | ASYNC_DSSS_RX_REFINE_DLL_SPACING 0.5 |
| define | ASYNC_DSSS_RX_REFINE_MIN_BLOCKS 7u |
Detailed Description¶
Unlike DsssReceiver, which goes straight from an acquisition hit to tracking on the hit's coarse Doppler estimate, this object refines the estimate between the two, closing the low-Es/N0 pull-in gap a coarse-only estimate leaves at large static Doppler offsets. Its design of record is docs/design/async-dsss-parallel.md: N of these receivers, always on, each searching and tracking by itself under a controller that only watches. The despreader, the states and the release under it are docs/design/async-dsss-receiver.md. Its states are the ASYNC_DSSS_RX_* values below, also status().state:
- searching (the start): samples feed the embedded
Acquisition. A hit is made into a seed bydp_acq_build_handoff()and taken throughdp_async_dsss_receiver_seed(), and the unconsumed tail of the same call goes straight on. The refine chain is a FROZEN carrier wipe (costas_wipeoff()at the coarse estimate,costas_update()never called) feeding a collectionDllwhosedp_dll_lookback_segments(refine_max_error_db)windows oversample each epoch the asynchronous data's residual carrier rides a ~symbol_rate-wide spectrum a single per-epoch dump would alias (seerefine_max_error_dbondp_async_dsss_receiver_create()) then aRateConvertertoCarrierAcquisition's rate, thenCarrierAcquisitionitself. - refining: samples feed the refine chain until
CarrierAcquisitionis ready or gives up; then the live tracking chain is built FRESH, from the hand-off's chip phase advanced by the clock dilation over the refine (not wherever the refine-stageDlldrifted) and the refined on a give-up, the seed's Doppler. - tracking: the pre-despread carrier loop (
costas_wipeoff/costas_update) ->Dll->RateConverter->MpskReceiver. The carrier loop updates once per code period from a NON-DATA-AIDED (squaring) discriminator over that period's coherent-I&D partials (adr_track_period()): a code period spans ~0.9 data symbols at SPEC's ratio, so a transition lands inside nearly every period, and squaring is what makes the error transition-robust. AtASYNC_DSSS_RX_BN_CARRIERthe loop pulls the refined seed in and removes the full coupled Doppler (offset and 500 Hz/s ramp) before despreading, soMpskReceiveris left a small residual. A pure PLL: no FLL. - lost: the emitter is gone. Without
reacquire, entered from tracking when BOTH lock flags have been down, without a break, for longer thanlost_confirm_s(docs/design/async-dsss-receiver.md §10, §11.2); the loops stop and samples are discarded untilreset().lost_confirm_s = 0(the default) never enters it. Withreacquirethe release is symbol lock alone and returns the receiver to searching instead, so it never reads lost (docs/design/async-dsss-parallel.md §3). While the confirm interval runs the loops HOLD what they settled on with both flags up rather than run on noise (dp_dll_set_coast(); docs/design/async-dsss-receiver.md §10; docs/design/async-dsss-receiver-measurements.md §12.19, doppler#1271), so a departed emitter's receiver stands where the emitter left it, and neither sweeps onto a neighbour's code nor restarts its release clock on one it followed. One flag down is a degrade: reported, and the loops run, which is what rides out two live emitters crossing each other's code phase.
dp_async_dsss_receiver_seed() hands a searching receiver a detection from outside: the record its own hit would have produced (a hit is a seed the object makes for itself). It refuses on a receiver that already holds one, so a hold is never silently overwritten.
The refine and track stages share ONE carrier-wipe scratch/carry buffer set (car_wiped_buf/car_carry_buf/car_carry_len, sized tsamps = code_len*spc); they never run concurrently. The carry holds 0..tsamps-1 live samples (a full period is processed at once, and set_state refuses tsamps); the blob packs its fixed tsamps region last, the live samples then zeros.
dp_async_dsss_receiver_state_t *rx = dp_async_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, 0, // segments, sps, differential
0.5, 4, 14.0, 64, 8, false, 100000, // refine_* tuning
0.0, // carrier_freq_hz (0 = aiding off)
0.0, // lost_confirm_s (0 = never lost)
false); // reacquire (off: lost waits)
float _Complex syms[4096];
size_t n = dp_async_dsss_receiver_steps(rx, x, x_len, syms, 4096);
dp_async_dsss_receiver_destroy(rx);
Public Types Documentation¶
enum async_dsss_rx_state_t¶
enum async_dsss_rx_state_t {
ASYNC_DSSS_RX_SEARCHING = 0,
ASYNC_DSSS_RX_REFINING = 1,
ASYNC_DSSS_RX_TRACKING = 2,
ASYNC_DSSS_RX_RESERVED = 3,
ASYNC_DSSS_RX_LOST = 4
};
Public Functions Documentation¶
function dp_async_dsss_receiver_configure_chain_raw¶
Pin the live-tracking despread/resample/demod grid directly. The escape hatch for the composition-specific knob: segments (the live Dll's tracking parameter) andsps /n (MpskReceiver's samples and terminal outputs per symbol) are independently overridable, still bridged by a freshly-sizedRateConverter and never coupled to each other. While searching/refining it re-pins the grid used to build the next tracking chain; once tracking it rebuildsdll /rc /rx in place, allocating every replacement before adopting it, and re-seeds the rebuilt chain from the whole carrier loop 1 plus loop 2's integrator, the sum status() reports so a retune keeps the signal it was tracking.
int dp_async_dsss_receiver_configure_chain_raw (
dp_async_dsss_receiver_state_t * state,
size_t segments,
size_t sps,
int n
)
The new grid is checked as create() checked the first, before anything is rebuilt, and refused with the receiver left on its prior grid: the Dll's domain at segments, sps >= 2, a symbol period of at most 2^20 Dll partials, which the Dll's aid would otherwise refuse mid-build (doppler#2103), and an n MpskReceiver would refuse (#2421). Still an abort: an allocation failure in the rebuild (doppler#2112).
Parameters:
stateMust be non-NULL.segmentsLive-tracking Dll segments per code period.spsMpskReceiver samples per symbol (the resample target), >= 2.nMpskReceiver's terminal outputs per symbol (m_out): inside dp_ratesync_m_ok() (even, 2..8), and dividingsps.
Returns:
0 on success, -1 on a grid refused as above.
>>> import numpy as np
>>> from doppler.dsss import AsyncDsssReceiver
>>> from doppler.wfm import Gold
>>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
>>> rx = AsyncDsssReceiver(code, chip_rate=3.069e6, symbol_rate=2700.0,
... spc=2, doppler_uncertainty=500.0)
>>> rx.configure_chain_raw(segments=6, sps=8, n=8) # re-pin the chain
>>> rx.segments # tracking grid updated in place
6
function dp_async_dsss_receiver_configure_lock_raw¶
Re-tune the live-tracking Dll's code-lock detector directly. Forwards to dp_dll_configure_lock_raw() on the live tracking Dll (the one behindget_code_locked() ), NOT the refine-stage collection Dll. It retunes the Dll the receiver holds now; every chain build (a seed, the hand-over, reset() or configure_chain_raw()) replaces that Dll with a freshly sized one, so call it once tracking. The detector is the hysteretic lockdet over the DLL's per-N-look CFAR statistic — the levels and verify counts trade declare latency against false-alarm rate.
void dp_async_dsss_receiver_configure_lock_raw (
dp_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
)
Parameters:
stateMust be non-NULL.up_threshCFAR-statistic level to declare code lock (hit when the statistic exceeds it).down_threshLevel below which a look is a miss; choose <=up_threshfor level hysteresis.n_looksLooks per decision — the DLL's non-coherent integration depth feeding one statistic.alphaEMA smoothing coefficient on the lock statistic (0..1); smaller is smoother/slower.n_upConsecutive hits required to declare lock.n_downConsecutive misses required to drop lock.>>> import numpy as np >>> from doppler.dsss import AsyncDsssReceiver >>> from doppler.wfm import Gold >>> code = np.asarray(Gold().generate(1023)).astype(np.uint8) >>> rx = AsyncDsssReceiver(code, chip_rate=3.069e6, symbol_rate=2700.0, ... spc=2, doppler_uncertainty=500.0) >>> 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 # replaced at the next chain build 0
function dp_async_dsss_receiver_configure_search_raw¶
Pin the embedded Acquisition's search grid directly. Forwards to dp_acq_configure_search_raw() __ the escape hatch under this object'ssymbol_rate -driven auto-sizing, for a power user who wants a specific(doppler_bins, n_noncoh) . The pin replaces the whole auto-sized grid, the window-tiled search included. Only meaningful while searching; the search does not run again until the nextreset() .
int dp_async_dsss_receiver_configure_search_raw (
dp_async_dsss_receiver_state_t * state,
size_t doppler_bins,
size_t n_noncoh
)
Parameters:
stateMust be non-NULL.doppler_binsThe coherent depth to pin, in[1, reps]: 1, as the receiver's continuous engine hasreps = 1.n_noncohNon-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 dp_acq_configure_search_raw).
>>> import numpy as np
>>> from doppler.dsss import AsyncDsssReceiver
>>> from doppler.wfm import Gold
>>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
>>> rx = AsyncDsssReceiver(code, chip_rate=3.069e6, symbol_rate=2700.0,
... spc=2, doppler_uncertainty=500.0)
>>> rx.configure_search_raw(doppler_bins=1, n_noncoh=16) # pin it
>>> rx.refining # still searching, on the pinned grid
0
function dp_async_dsss_receiver_create¶
Create an AsyncDsssReceiver in the searching state.
dp_async_dsss_receiver_state_t * dp_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,
bool reacquire,
double code_only_s,
double doppler_rate
)
Only code/chip_rate/symbol_rate describe the signal itself. The refine_* defaults are sized to close SPEC's 4-5 dB, 500 Hz/s combined scenario as they are; a power user can override each one.
Parameters:
codeSpreading 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_lenChips incode.chip_rateChip rate, Hz.symbol_rateData-symbol rate, Hz: finite, > 0, at mostchip_rate, and at most 2^20 Dll partials per symbol atsegments.spcSamples/chip; default 2.mPSK order, 2/4/8; default 2 (BPSK).cn0_dbhzDesign C/N0, dB-Hz; default 55.0 feeds BOTH the embedded Acquisition's own sizing AND (derated byrefine_design_margin_db) CarrierAcquisition'sdesign_snr.pfaAcquisition false-alarm target; default 1e-3. Also CarrierAcquisition's ownpfa.pdAcquisition detection-probability target; default 0.9. Also CarrierAcquisition's ownpd.doppler_uncertaintyOne-sided Doppler search half-range, Hz; default 100.0.segmentsLive-tracking Dll's own segments; default 4.spsMpskReceiver's samples/symbol; default 8.differentialMpskReceiver's differential demap; default 0 (coherent). It acts in MpskReceiver's bits(), which this object does not offer, so steps()' symbols are the same either way (#2471).refine_max_error_dbMax tolerable async-lookback correlation-power loss driving the refine-stage collection Dll's coherent-I&D window count via dp_dll_lookback_segments(). Oversampling the epoch is required for the asynchronous data: the residual carrier rides a ~symbol_rate-wide data-modulated spectrum, so segments>1 (default yields 11 at tsamps=2046) samples it above Nyquist; segments=1 undersamples and aliases it. Default 0.5.refine_samples_per_symbolCarrierAcquisition's own operating rate = this * symbol_rate; default 4.refine_design_margin_dbEmpirical derating of cn0_dbhz before CarrierAcquisition's design_snr; default 14.0.refine_n_fftCarrierAcquisition's own block size; default 64.refine_zero_padCarrierAcquisition's own zero_pad; default 8.refine_sequentialCarrierAcquisition's sequential mode; default false its early per-block test fires on far too little averaging at SPEC's Es/N0 floor (as few as 4 blocks, 150-200+ Hz off), so false waits the full design_snr-derived dwell.refine_max_n_blocksCarrierAcquisition's give-up cap in sequential mode, and the cap on the dwell in either mode; default 100000.carrier_freq_hzNominal RF carrier frequency, Hz, enabling carrier->code aiding; 0.0 (default) = off. When > 0, the coupled code-rate Doppler (carrier_offset/carrier_freq) is fed to the tracking Dll via dp_dll_set_rate_aid() so the code loop rides a dilated clock the discriminator alone can't pull in at low SNR. Set to the receiver's own downlink RF frequency for a physically-coupled Doppler capture. Coupled, it is above half the sample rate (dp_acq_carrier_freq_ok()).lost_confirm_sRelease rule: both lock flags down, continuously, for longer than this many seconds puts the receiver in the lost state (see get_lost()). Size it past the longest fade the link must ride. The clock also runs from the first tracking sample, when neither flag is up yet, so a hand-off that never locks within the interval is released the same way as an emitter that leaves. Default 0.0 = never the exit is then reset().reacquireRelease on symbol lock alone, and search again by itself. With it set, the release clock counts samples of tracking with the SYMBOL lock flag down (code lock is not consulted), and when it passes lost_confirm_s the receiver resets itself to searching instead of entering lost: it never waits for reset(). A pool of receivers that are always online uses it. The clock runs from the first tracking sample, as above, so a track that never locks (a noise false alarm) is released too; size lost_confirm_s past the pull-in time to symbol lock, or a slow lock is released before it comes. Requires a lost_confirm_s of at least one input sample, since a release that never fires would never search again; a blob in the lost state is refused by set_state(). Default false: the release rule above.code_only_sSeconds of code-only signal the waveform holds at any chip phase (a symbol-free window; design async-dsss-parallel.md §4), >= 0 and finite. The receiver hands its searchercode_only_epochs = floor(code_only_s * chip_rate / code_len) - 1(at least 1), and the searcher's slow-time depth follows:coherent_bins = D, at most(code_only_epochs + 1)/2, rounded down to a 7-smooth length (dp_acq_create_continuous()). Default 0: no window, D = 1.doppler_rateDoppler rate in Hz/s, >= 0 and finite, that the slow-time depth is bounded against: at mostf_epoch / sqrt(2 * doppler_rate)epochs (dp_acq_create_continuous()). Default 0: the window is the only bound.
Returns:
A new receiver, searching; NULL on an invalid argument, including reacquire with a lost_confirm_s under one sample.
>>> import numpy as np
>>> from doppler.dsss import AsyncDsssReceiver
>>> from doppler.wfm import Gold
>>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
>>> rx = AsyncDsssReceiver(code, chip_rate=3.069e6, symbol_rate=2700.0,
... spc=2, doppler_uncertainty=500.0)
>>> (rx.tracking, rx.refining, rx.lost) # searching
(0, 0, 0)
function dp_async_dsss_receiver_destroy¶
Destroy a receiver and release every child.
Parameters:
stateMay be NULL.
function dp_async_dsss_receiver_get_car_last_error¶
Pre-despread Costas phase discriminator (rad): the residual carrier phase LOOP 1 (which de-rotates before the Dll) is not nulling.
function dp_async_dsss_receiver_get_car_nco_freq¶
LOOP 1 (pre-despread Costas) loop-filter output = NCO frequency command, cycles/sample of the front-end (chip_rate*spc) rate.
function dp_async_dsss_receiver_get_chip_phase¶
The live Dll's code phase, chips, in the Dll's instantaneous convention the mirror ofacq_result_t::code_phase 's correlation-lag convention (dp_acq_build_handoff() ). While refining there is no live Dll: it is the seed's phase advanced by the clock dilation since, the phase the hand-over starts the live chain from.
function dp_async_dsss_receiver_get_cn0_dbhz_est¶
The seed's C/N0 estimate, dB-Hz, unchanged until reset().
function dp_async_dsss_receiver_get_code_locked¶
Binary code-lock flag from the live tracking Dll's own verify-counted (pfa-tuned) lock detector — the fundamental DSSS "am I despreading" lock, de-chattered by up/down hysteresis.
function dp_async_dsss_receiver_get_code_rate¶
The Dll's code rate, chips per nominal chip (~1.0 at lock), without the carrier aid; a coasting Dll reports its held rate.
function dp_async_dsss_receiver_get_coherent_bins¶
The searcher's slow-time depth D: epochs per coherent block.
Derived at create from code_only_s and doppler_rate (dp_async_dsss_receiver_create()); 1 without a code-only window. Every restarted search reset(), a release with reacquire, a refused hand-over, configure_search_raw() starts its next dwell on a multiple of D * code_len * spc input samples counted from create: the receiver discards input up to that boundary first, at most one block. Receivers fed the same stream from create therefore search on the same block grid.
Parameters:
stateMust be non-NULL.
Returns:
D, >= 1.
>>> import numpy as np
>>> from doppler.dsss import AsyncDsssReceiver
>>> from doppler.wfm import Gold
>>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
>>> rx = AsyncDsssReceiver(code, chip_rate=3.069e6, symbol_rate=2700.0,
... spc=2, doppler_uncertainty=500.0)
>>> rx.coherent_bins # no code-only window
1
>>> rx = AsyncDsssReceiver(code, chip_rate=3.069e6, symbol_rate=2700.0,
... spc=2, doppler_uncertainty=500.0,
... code_only_s=0.0627)
>>> rx.coherent_bins # 187 epochs: 94, run 7-smooth
90
function dp_async_dsss_receiver_get_doppler_hz¶
The Doppler estimate the chain was built from, Hz: the seed's while refining, the CarrierAcquisition-refined value once tracking. The live carrier is status().doppler_hz.
function dp_async_dsss_receiver_get_lock¶
The carrier lock statistic of the MpskReceiver this receiver tracks with: the EMA of its M-th-power lock signal, near 1 when locked and near 0 on noise.
function dp_async_dsss_receiver_get_lock_metric¶
Symbol-lock metric = SNR-weighted EMA of (I^2-Q^2)/(I^2+Q^2) = cos(2*phi) over the emitted symbols (locked -> ~+1). Drives locked .
function dp_async_dsss_receiver_get_lock_threshold¶
The lock-metric threshold locked is declared strictly above (0.5, ASYNC_DSSS_RX_LOCK_UP); exposed alongside lock_metric for engineering debug.
function dp_async_dsss_receiver_get_locked¶
The receiver's symbol-lock flag: a hysteretic detector over get_lock_metric(), declared after 30 consecutive symbols strictly above get_lock_threshold() (0.5) and dropped after 15 consecutive symbols strictly below 0.3 (ASYNC_DSSS_RX_LOCK_N_UP, _UP, _N_DOWN and _DOWN, fixed at create). The flag the release rule and a pool's holder act on.
function dp_async_dsss_receiver_get_lost¶
1 once the release rule has fired: both lock flags were down, continuously, for longer than lost_confirm_s while tracking an emitter that left, or a seed that never locked. The loops have stopped and samples are consumed and discarded; the holder releases the assignment and calls reset(). 0 in every other state, and always 0 withlost_confirm_s = 0 .
function dp_async_dsss_receiver_get_mpsk_last_error¶
MpskReceiver carrier phase discriminator (rad): the residual carrier phase LOOP 2 (post-despread) is not nulling.
function dp_async_dsss_receiver_get_n¶
function dp_async_dsss_receiver_get_n_exclusions¶
Code phases set_exclusions() holds.
function dp_async_dsss_receiver_get_nco_freq¶
The MpskReceiver's carrier NCO command, its loop filter's whole output, cycles/sample at its input rate ( sps * symbol_rate ). Its mean tracks a Doppler ramp with no lag (unlike get_norm_freq()); its variance is the carrier loop's stress.
function dp_async_dsss_receiver_get_norm_freq¶
The post-despread carrier the MpskReceiver tracks: its centre plus its loop's integrator, cycles/sample at its input rate ( sps * symbol_rate ). Lags a Doppler ramp by the Type-II ramp error.
function dp_async_dsss_receiver_get_rank¶
Which surviving peak of a dwell the search takes; 0 = the strongest. Set with dp_async_dsss_receiver_set_rank() .
function dp_async_dsss_receiver_get_refining¶
1 while refining: the CarrierAcquisition dwell; 0 in every other state.
function dp_async_dsss_receiver_get_segments¶
function dp_async_dsss_receiver_get_sps¶
function dp_async_dsss_receiver_get_state¶
function dp_async_dsss_receiver_get_threads¶
Workers the searcher runs, the calling thread included; 1 = serial. Set with dp_async_dsss_receiver_set_threads() .
function dp_async_dsss_receiver_get_tracking¶
1 once the live tracking chain is built and demodulating; 0 in every other state.
function dp_async_dsss_receiver_reset¶
Return to the searching state. Resets the embedded Acquisition and rebuilds the refine-stage chain and the live tracking chain back to their placeholder seed (phase 0, no Doppler). A receiver that has locked cannot be "reset back to tracking the same signal," only back to searching every object's reset() semantics in this codebase.
Parameters:
stateMust be non-NULL.>>> import numpy as np >>> from doppler.dsss import AsyncDsssReceiver >>> from doppler.wfm import Gold >>> code = np.asarray(Gold().generate(1023)).astype(np.uint8) >>> rx = AsyncDsssReceiver(code, chip_rate=3.069e6, symbol_rate=2700.0, ... spc=2, doppler_uncertainty=500.0) >>> rx.reset() # abort any lock, hunt from scratch >>> (rx.tracking, rx.refining, rx.chip_phase) # all cleared (0, 0, 0.0)
function dp_async_dsss_receiver_seed¶
Take a detection from outside and start refining from it.
int dp_async_dsss_receiver_seed (
dp_async_dsss_receiver_state_t * state,
double chip_phase,
double doppler_hz_est,
double cn0_dbhz_est
)
The hand-off of docs/design/async-dsss-receiver.md §2.2: the three numbers a searcher's hit carries that this receiver uses acq_handoff_t's chip_phase, doppler_hz_est and cn0_dbhz_est exactly as its own hit would have produced them (steps() calls this on its own hit). chip_phase is the code's instantaneous phase in chips, Dll's convention, at the FIRST sample of the next steps() call; dp_acq_build_handoff() makes it from a hit's lag. The refine chain is rebuilt from the seed and the state becomes refining; the unconsumed tail is the caller's to feed.
Refused (DP_ERR_INVALID, nothing changes) on a receiver that is not waiting for one refining, tracking or lost because "assigned once" is a property of the object, not of the caller's bookkeeping; reset() releases it. Accepted while searching: an outside hit simply beats the receiver's own. Also refused outside the seed domain, dp_async_dsss_receiver_seed_ok().
Parameters:
stateMust be non-NULL.chip_phaseCode phase at the next sample, chips, in[0, code_len).doppler_hz_estCoarse Doppler estimate, Hz (the refine stage sharpens it).cn0_dbhz_estThe hit's C/N0 estimate, dB-Hz; reported back by get_cn0_dbhz_est(), unchanged until reset().
Returns:
DP_OK, or DP_ERR_INVALID when refused.
>>> import numpy as np
>>> from doppler.dsss import AsyncDsssReceiver
>>> from doppler.wfm import Gold
>>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
>>> rx = AsyncDsssReceiver(code, chip_rate=3.069e6,
... symbol_rate=2700.0, spc=2)
>>> rx.seed(chip_phase=512.25, doppler_hz_est=-1500.0,
... cn0_dbhz_est=48.0)
>>> (rx.refining, rx.doppler_hz, rx.cn0_dbhz_est)
(1, -1500.0, 48.0)
Already assigned -- refused until reset():
>>> rx.seed(0.0, 0.0, 48.0) # doctest: +ELLIPSIS
Traceback (most recent call last):
...
ValueError: seed refused: ...
>>> rx.reset()
A chip phase must be inside the code, `[0, code_len)`:
>>> rx.seed(1023.0, 0.0, 48.0) # doctest: +ELLIPSIS
Traceback (most recent call last):
...
ValueError: seed refused: ...
function dp_async_dsss_receiver_seed_ok¶
The seed domain: whether (chip_phase, doppler_hz_est) is onedp_async_dsss_receiver_seed() accepts, and one a live receiver can hold.
int dp_async_dsss_receiver_seed_ok (
const dp_async_dsss_receiver_state_t * state,
double chip_phase,
double doppler_hz_est
)
chip_phase must be finite and inside [0, code_len), and doppler_hz_est must be strictly inside (-fs/2, fs/2), where fs is chip_rate * spc. A Doppler at or past half the sample rate is not reportable (dp_acq_carrier_freq_ok() relies on this bound), and a seed there sent the hand-off's phase to NaN (doppler#2103).
seed() refuses by this predicate, set_state() restores the seed fields only inside it, the searching receiver's hit path does not seed from a hit it fails, and the hand-over holds the REFINED Doppler to it too: a seed in range whose residual carries it to or past fs/2 is refused at the hand-over, counted in refused_hits, and the receiver searches again rather than track a Doppler it cannot report. It reads only the receiver's fixed config (code length, chip rate, spc), so any receiver built from the same config gives the same answer.
Parameters:
stateMust be non-NULL.chip_phaseCode phase, chips.doppler_hz_estDoppler estimate, Hz.
Returns:
1 inside the seed domain, 0 outside it (a NaN is outside).
function dp_async_dsss_receiver_set_exclusions¶
Code phases the search must not take: signals another receiver already tracks.
int dp_async_dsss_receiver_set_exclusions (
dp_async_dsss_receiver_state_t * state,
const double * phases,
size_t phases_len
)
Replaces the list. A peak within one chip, circularly, of any of them is dropped from a dwell's list at any Doppler, before ranking (set_rank()). Phases are chips in status().chip_phase's convention and are folded into [0, code_len). An empty list (the default) excludes nothing. A tracking receiver is not affected: the list applies to its next search. A mutator's value, so it travels in the state blob (#2022).
Parameters:
stateMust be non-NULL.phasesCode phases, chips; finite. May be NULL whenphases_lenis 0.phases_lenAt most ASYNC_DSSS_RX_MAX_EXCLUSIONS.
Returns:
DP_OK; DP_ERR_INVALID (list unchanged) on a non-finite phase or too many.
>>> import numpy as np
>>> from doppler.dsss import AsyncDsssReceiver
>>> rx = AsyncDsssReceiver(code=[1, 0, 1, 1, 0, 0, 1], chip_rate=1e6,
... symbol_rate=1e6 / 28.0, spc=4, cn0_dbhz=60.0)
>>> rx.set_exclusions(np.array([2.5, 9.0])) # 9 folds to 2
>>> rx.n_exclusions
2
function dp_async_dsss_receiver_set_rank¶
Take the rank-th surviving peak of a dwell, not the strongest.
The searcher reports up to ASYNC_DSSS_RX_MAX_PEAKS peaks per dwell, strongest first. The receiver drops every peak within one chip (circularly) of a stronger one, so one signal's neighbouring cells hold one place; then every peak a stronger signal's own sidelobes could explain; then every peak within one chip of an excluded code phase (set_exclusions()). The survivors are ranked from 0. A dwell with no peak at this rank hands nothing off and the search goes on.
The sidelobe rule: a strong signal's list also holds its code's Doppler-offset sidelobes, up to dp_acq_dsl_db() below it and hundreds of chips from its phase (design acq-surface.md §8). A peak counts as a second signal only if its test_stat exceeds the stronger signals' summed test_stat, times that level, by more than the detector's own gate (dp_acq_gate_stat()). A sidelobe then passes only when the noise on it alone would have cleared the gate, the rate the search already runs at. What it costs is near-far: a second signal much more than dp_acq_dsl_db() below a stronger one (-16.95 dB on the SPEC Gold-1023 search) is not taken while the stronger one is on the air (async-dsss-parallel.md §3). Searchers that share a stream and its block grid (get_coherent_bins()) and hold ranks 0..k-1 therefore take k different signals in one dwell. Takes effect at the next dwell. A mutator's value, so it travels in the state blob (#2022).
Parameters:
stateMust be non-NULL.rank0 (the strongest, the default) to ASYNC_DSSS_RX_MAX_PEAKS - 1.
Returns:
DP_OK; DP_ERR_INVALID (rank unchanged) past the list.
>>> from doppler.dsss import AsyncDsssReceiver
>>> rx = AsyncDsssReceiver(code=[1, 0, 1, 1, 0, 0, 1], chip_rate=1e6,
... symbol_rate=1e6 / 28.0, spc=4, cn0_dbhz=60.0)
>>> rx.set_rank(1)
>>> rx.rank
1
function dp_async_dsss_receiver_set_refine_min_blocks¶
Floor the refine's dwell at n_blocks , whatever the detection sizing asks (docs/design/async-dsss-receiver-measurements.md §12.16, #1265).
int dp_async_dsss_receiver_set_refine_min_blocks (
dp_async_dsss_receiver_state_t * state,
size_t n_blocks
)
CarrierAcquisition's dwell is sized for DETECTION at the derated C/N0 (cn0_dbhz - refine_design_margin_db), so it shortens as the C/N0 rises two blocks at 45 dB-Hz with the shipped margin while the noise of the estimate it hands the tracking chain does not shorten with it: 210 Hz at two blocks against a chain that pulls in from a few hundred, so one hand-over in sixty landed outside and tracked the code with the carrier never locked. Seven blocks (42 ms, the default and the floor dwell of docs/design/async-dsss-receiver-measurements.md §12.10) hold the estimate to 77 Hz. Applied to the next refine chain built a receiver already refining keeps its dwell. A mutator's value, so it travels in the state blob (#2022). n_blocks of 0 removes the floor; the value is clamped by refine_max_n_blocks where that cap is lower.
Parameters:
stateMust be non-NULL.n_blocksThe floor, blocks.
Returns:
DP_OK.
>>> from doppler.dsss import AsyncDsssReceiver
>>> rx = AsyncDsssReceiver(code=[1, 0, 1, 1, 0, 0, 1], chip_rate=1e6,
... symbol_rate=1e6 / 28.0, spc=4, cn0_dbhz=60.0)
>>> rx.refine_min_blocks # the default floor
7
>>> rx.set_refine_min_blocks(12)
>>> rx.refine_min_blocks
12
function dp_async_dsss_receiver_set_state¶
function dp_async_dsss_receiver_set_telemetry¶
Attach (or detach) a telemetry context across the receiver's loops, and register the receiver's own lifecycle and symbol-lock decision.
int dp_async_dsss_receiver_set_telemetry (
dp_async_dsss_receiver_state_t * state,
dp_tlm_t * tlm,
const char * prefix,
uint32_t decim
)
The receiver reports under the prefix, once per steps() call that carries samples, after the call, so on the time axis at the stamp the caller set for that block (dp_tlm_set_now()): * "<prefix>.state" where the call left the receiver, the ASYNC_DSSS_RX_* value: 0 searching, 1 refining, 2 tracking, 4 lost (status().state); * "<prefix>.handover_hz" the Doppler the chain was built from, Hz (dp_async_dsss_receiver_get_doppler_hz()): 0 before a seed, the seed's while refining, the refined value from the hand-over on, and 0 again after a refused hand-over or a reset(), which return the receiver to searching. A record is a float, so it resolves about 0.5 Hz near 5 MHz. Not the live carrier: status().doppler_hz reads that, and ".car.freq" and ".car.nco" are the carrier loop's own view of it, in cycles a sample at the input rate, so times chip_rate * spc for Hz; * "<prefix>.refused" the hits refused as seeds so far (status().refused_hits).
Feed blocks of about a code period, as the examples do, and these read as a time series of the lifecycle: when the search hit, how long the refine took, what it handed over, when the release came.
Each loop reports under the prefix, at its own update rate: * "<prefix>.car" the chip-domain carrier loop (dp_costas_set_telemetry(): ".lock", ".e", ".freq", ".locked", ".nco"), once per carrier update: once a code period, except while it coasts, when loop 1 is held; * "<prefix>.code" the code loop (dp_dll_set_telemetry(): ".e", ".rate", ".lock", ".locked", ".nco"), once per discriminator update; * "<prefix>.demod" the symbol-domain demodulator (dp_mpsk_receiver_set_telemetry(): its lock, its carrier and timing loops, the symbols, its AGC), once per recovered symbol (the AGC's pair once per gain update); * "<prefix>.acq" the acquisition engine (dp_acq_set_telemetry()), once per decided dwell; * "<prefix>.rc" the rate converter between the code loop and the demodulator (dp_RateConverter_set_telemetry()). It reports only an AGC, and this receiver builds it without one, so it registers no probes; * "<prefix>.sym.lock" and "<prefix>.sym.locked" the receiver's own symbol-lock metric (dp_async_dsss_receiver_get_lock_metric()) and its decision (dp_async_dsss_receiver_get_locked()), the one its users act on, once per emitted symbol.
The carrier and code loops report their filters two ways: ".car.freq" and ".code.rate" (and the demodulator's ".demod.car.freq") are the integrators, each loop's frequency memory, which a coast holds; ".car.nco", ".code.nco" and ".demod.car.nco" are the complete commands the NCOs run at, the proportional term (and the code loop's carrier aid) included. ".demod.sync.rate" is the timing loop's output, and ".e" is each discriminator's input to its filter.
The attachment is a REQUEST kept on the receiver: every seed rebuilds the tracking chain, and the rebuilt loops are attached again under the same names (so the same probe ids). Records are therefore continuous across re-seeds, and a reset keeps the attachment too.
Setup path, never hot; the context is borrowed and must outlive the attachment, and the receiver must step on one thread (SPSC rules in dp_tlm/dp_tlm_core.h). Passing NULL detaches every loop.
Parameters:
stateMust be non-NULL.tlmTelemetry context to attach, or NULL to detach.prefixProbe-name prefix; NULL means "rx".decimEmit every decim-th event on each probe; >= 1.
Returns:
DP_OK, or DP_ERR_INVALID on decim 0, a prefix too long for some probe's name refused before anything registers on tlm, because the loops are first attached to a scratch context that finds the refusal or a probe table with no room for every probe (found the same way, from the names the scratch holds). Every refusal leaves tlm's table untouched and the receiver as it was: detached, or still on its previous context under its previous prefix.
>>> import numpy as np
>>> from doppler.dsss import AsyncDsssReceiver
>>> from doppler.telemetry import Telemetry
>>> from doppler.wfm import Gold
>>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
>>> rx = AsyncDsssReceiver(code, chip_rate=3.069e6, symbol_rate=2700.0,
... spc=2, doppler_uncertainty=500.0)
>>> tlm = Telemetry(1 << 12)
>>> rx.set_telemetry(tlm, "rx")
>>> sorted(p for p in tlm.probe_names if p.startswith("rx.c"))
['rx.car.e', 'rx.car.freq', 'rx.car.lock', 'rx.car.locked', 'rx.car.nco', 'rx.code.e', 'rx.code.lock', 'rx.code.locked', 'rx.code.nco', 'rx.code.rate']
>>> sorted(p for p in tlm.probe_names if p.startswith(("rx.s", "rx.h",
... "rx.r")))
['rx.handover_hz', 'rx.refused', 'rx.state', 'rx.sym.lock', 'rx.sym.locked']
>>> rx.set_telemetry(None, "rx") # detach
function dp_async_dsss_receiver_set_threads¶
Set the workers the searcher fans its Doppler tiles across.
Forwards to dp_acq_set_threads() on the receiver's searcher. The search is bit-identical at every count, so this changes the cost of a steps() call and nothing about its output. A pool that steps its receivers in parallel sets each one to 1. A mutator's value, so it travels in the state blob (#2022). The blob carries the count that RAN, not the request: set_threads(0) on a 20-core writer restores as 20 workers on any reader, by design a resumed receiver runs as the one that was checkpointed, and a pool's 1 is 1 everywhere. Setup path; not while another thread is inside steps().
Parameters:
stateMust be non-NULL.nThread count; 0 = online cores, 1 = serial.
Returns:
DP_OK. The count actually running is dp_async_dsss_receiver_get_threads(); a searcher with one Doppler tile always runs 1.
>>> from doppler.dsss import AsyncDsssReceiver
>>> rx = AsyncDsssReceiver(code=[1, 0, 1, 1, 0, 0, 1], chip_rate=1e6,
... symbol_rate=1e6 / 28.0, spc=4, cn0_dbhz=60.0)
>>> rx.set_threads(1)
>>> rx.threads
1
function dp_async_dsss_receiver_state_bytes¶
function dp_async_dsss_receiver_status¶
Read the status record, by value: the state; where the emitter is now (the live carrier), its chip phase, code rate and C/N0; both lock flags with the symbol-lock metric and threshold; both carrier residuals; and the two clocks in input samples (since the state was entered; both flags down without a break).
async_dsss_receiver_status_t dp_async_dsss_receiver_status (
const dp_async_dsss_receiver_state_t * state
)
Cheap and allocation-free: every field is a read of live state. The one-at-a-time getters report the same fields, doppler_hz apart (see the record); this is the face a controller reads. No timestamp: the clocks are in input samples, and the holder, which owns the sample clock, stamps the record.
Parameters:
stateMust be non-NULL.
Returns:
The record, by value.
>>> import numpy as np
>>> from doppler.dsss import AsyncDsssReceiver
>>> from doppler.wfm import Gold
>>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
>>> rx = AsyncDsssReceiver(code, chip_rate=3.069e6,
... symbol_rate=2700.0, spc=2)
>>> st = rx.status()
>>> (st.state, st.doppler_hz, st.code_locked, st.locked) # searching
(0, 0.0, 0, 0)
>>> rx.seed(chip_phase=100.0, doppler_hz_est=-250.0, cn0_dbhz_est=50.0)
>>> st = rx.status()
>>> (st.state, round(st.doppler_hz, 6), st.cn0_dbhz_est) # refining
(1, -250.0, 50.0)
>>> _ = rx.steps(np.zeros(2046, np.complex64))
>>> rx.status().state_samples # since seed
2046
function dp_async_dsss_receiver_steps¶
Stream raw cf32 samples; emit demodulated symbols once tracking.
size_t dp_async_dsss_receiver_steps (
dp_async_dsss_receiver_state_t * state,
const float _Complex * x,
size_t x_len,
float _Complex * out,
size_t max_out
)
Drives the state machine. It emits nothing while searching or refining (an empty return is normal, not an error): a hit seeds the frozen-carrier refine chain, CarrierAcquisition sharpens the coarse Doppler estimate, and only once it is ready (or gives up) is the live tracking chain built and demodulation begins. A hit inside a call hands the rest of the call straight to the refine chain, so no sample is dropped there; at the refine's end, what the refine did not consume of the last call goes on to the live chain. Accepts any block size; state carries across calls, so a capture can be fed in frames of any length with no seam. Lost (after the release rule fires) consumes the samples and emit nothing, so the feeding loop is the same in every state; while tracking, the release clock runs on the two lock flags after every call, or on the symbol lock flag alone with reacquire (see lost_confirm_s). Tracking emits symbols whether or not they are data: a noise false alarm can reach tracking and emit until the release rule lets it go or, at the default lost_confirm_s of 0, until reset() and symbol lock (locked) is what says the symbols are a signal's, so read it rather than the count (#2439). Under SPEC's coupled offset + 500 Hz/s Doppler ramp the pre-despread Costas removes the full carrier dynamics before the code loop, so the recovered constellation lands cleanly on the BPSK real axis.
Parameters:
stateMust be non-NULL.xInput cf32 samples.x_lenNumber of input samples.outOutput symbols; caller providesmax_outcapacity.max_outOutput capacity. steps() never writes past it, and the symbols past it are dropped, not held for the next call.x_lenis not always enough: a call that completes a carried code period emits the whole period's symbols (#2469).
Returns:
Number of symbols written: 0 while searching, refining or lost, or before a full symbol's worth of input.
>>> import numpy as np
>>> from doppler.dsss import AsyncDsssReceiver
>>> from doppler.wfm import Gold
>>> sf, chip, sym, spc = 1023, 3.069e6, 2700.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(21)
>>> n = int(600 * tsym) + 4 * te # 600 async BPSK symbols
>>> idx = np.arange(n)
>>> data = (rng.integers(0, 2, 604) * 2 - 1).astype(float)
>>> si = np.clip((idx / tsym).astype(int), 0, 603)
>>> t = idx / fs
DSSS chips on a carrier sweeping at 500 Hz/s — the ramp the async
receiver has to track:
>>> sig = (data[si] * csign[(idx // spc) % sf]
... * np.exp(1j * 2 * np.pi * 0.5 * 500.0 * t * t))
>>> cn0 = 20.0 + 10 * np.log10(sym) # Es/N0 = 20 dB
>>> sigma = np.sqrt(fs / 10 ** (cn0 / 10))
>>> pre = 5 * te # noise-only lead-in
>>> 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 = AsyncDsssReceiver(
... code, chip_rate=chip, symbol_rate=sym, spc=spc,
... cn0_dbhz=cn0, doppler_uncertainty=500.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 # searched, refined, now tracking
1
>>> len(syms) > 300 # symbols recovered under the ramp
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 dp_async_dsss_receiver_steps_max_out¶
The output capacity steps() needs beyond the input's.
Returns 0, which is NOT a bound. A call that completes a code period carried over from earlier calls emits all of that period's symbols (code_len times symbol_rate over chip_rate of them), even if it brings one sample. At more than one symbol per period a call can therefore emit more symbols than its x_len, and those past max_out are dropped (see dp_async_dsss_receiver_steps()). A true bound is #2469.
Parameters:
stateMust be non-NULL.
Returns:
Macro Definition Documentation¶
define ASYNC_DSSS_RECEIVER_STATE_MAGIC¶
define ASYNC_DSSS_RECEIVER_STATE_VERSION¶
#define ASYNC_DSSS_RECEIVER_STATE_VERSION `13u /* v13: rank, exclusions; v12: samples_in, the dwell alignment, threads; v11: no cell mode; v10: the dll children grew (output level, #2495); v9: refine_min_blocks; v8: refused_hits; v7: no hand-off flavor; v6: the cell pull-in */`
define ASYNC_DSSS_RX_BN_CARRIER¶
define ASYNC_DSSS_RX_CARRIER_PULLIN_HZ¶
#define ASYNC_DSSS_RX_CARRIER_PULLIN_HZ (
chip_rate,
code_len
) `( ASYNC_DSSS_RX_BN_CARRIER * (chip_rate) / (2.0 * (double)(code_len)))`
The pre-despread carrier loop's acquisition bound, Hz: bn / m cycles per sample over its one-code-period update (m = 2, the squaring discriminator), native/validation/costas_pullin.c's measured 100% line reliable to twice it, dead by four. 97.8 Hz at 5 Mcps over Gold-1023.
define ASYNC_DSSS_RX_DEMOD_BN_CARRIER¶
define ASYNC_DSSS_RX_DEMOD_BN_TIMING¶
define ASYNC_DSSS_RX_DEMOD_LOCK_THRESH¶
define ASYNC_DSSS_RX_DEMOD_RRC_BETA¶
define ASYNC_DSSS_RX_DEMOD_RRC_SPAN¶
define ASYNC_DSSS_RX_DEMOD_ZETA¶
define ASYNC_DSSS_RX_DLL_BN¶
define ASYNC_DSSS_RX_DLL_SPACING¶
define ASYNC_DSSS_RX_DLL_ZETA¶
define ASYNC_DSSS_RX_LOCK_DOWN¶
define ASYNC_DSSS_RX_LOCK_DWELL¶
define ASYNC_DSSS_RX_LOCK_N_DOWN¶
define ASYNC_DSSS_RX_LOCK_N_UP¶
define ASYNC_DSSS_RX_LOCK_UP¶
define ASYNC_DSSS_RX_MAX_EXCLUSIONS¶
Most code phases dp_async_dsss_receiver_set_exclusions() holds: one per signal another receiver tracks.
define ASYNC_DSSS_RX_MAX_PEAKS¶
Peaks the searcher reports per dwell (dp_acq_set_max_peaks()): the list the receiver filters by exclusion, de-duplicates and ranks (dp_async_dsss_receiver_set_rank()). A rank is below this.
define ASYNC_DSSS_RX_REFINE_DLL_SPACING¶
define ASYNC_DSSS_RX_REFINE_MIN_BLOCKS¶
Default floor on the refine's dwell, blocks: the floor dwell of docs/design/async-dsss-receiver-measurements.md §12.10 (77 Hz of estimate noise at 45 dB-Hz); see dp_async_dsss_receiver_set_refine_min_blocks().
The documentation for this class was generated from the following file native/inc/doppler/async_dsss_receiver/async_dsss_receiver_core.h