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 -> handoff -> CarrierAcquisition refine -> Costas/Dll/RateConverter/ MpskReceiver track, one object. More...
#include "RateConverter/RateConverter_core.h"#include "acq/acq_core.h"#include "carrier_acq/carrier_acq_core.h"#include "cic/cic_core.h"#include "costas/costas_core.h"#include "dll/dll_core.h"#include "dp_state.h"#include "hbdecim/hbdecim_core.h"#include "lockdet/lockdet_core.h"#include "mpsk_receiver/mpsk_receiver_core.h"#include "resamp/resamp_core.h"#include "resample/resample_core.h"#include <complex.h>#include <stdbool.h>#include <stddef.h>#include "psd/psd_core.h"#include "detector/detector_core.h"#include "detection/detection_core.h"#include "spectral/spectral_core.h"#include "corr/corr_core.h"#include "fft/fft_core.h"#include "acc_trace/acc_trace_core.h"#include "ber/ber_core.h"#include "doppler_channel/doppler_channel_core.h"
Classes¶
| Type | Name |
|---|---|
| struct | async_dsss_receiver_extra_t |
| struct | async_dsss_receiver_state_t Composed receiver state. |
| struct | async_dsss_receiver_status_t One consistent picture of what the receiver is doing, by value. |
Public Functions¶
| Type | Name |
|---|---|
| int | async_dsss_receiver_configure_chain_raw (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 sample-rate/carrier-arm parameters) 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 so a failed pin leaves the receiver usable on its prior grid. |
| void | async_dsss_receiver_configure_lock_raw (async_dsss_receiver_state_t * state, double up_thresh, double down_thresh, size_t n_looks, double alpha, uint32_t n_up, uint32_t n_down) Re-tune the live-tracking Dll's code-lock detector directly. Forwards to dll_configure_lock_raw() on the live tracking Dll (the one behindget_code_locked() ), NOT the refine-stage collection Dll. Only meaningful once tracking has begun; a no-op while searching or refining. 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 | async_dsss_receiver_configure_search_raw (async_dsss_receiver_state_t * state, size_t doppler_bins, size_t n_noncoh) Pin the embedded Acquisition's search grid directly. Forwards to acq_configure_search_raw() — the escape hatch under this object'ssymbol_rate -driven auto-sizing, for a power user who wants a specific(doppler_bins, n_noncoh) . Only meaningful while searching; the acquisition search does not run again until the nextreset() . |
| async_dsss_receiver_state_t * | async_dsss_receiver_create (const uint8_t * code, size_t code_len, double chip_rate, double symbol_rate, size_t spc, int m, double cn0_dbhz, double pfa, double pd, double doppler_uncertainty, size_t segments, size_t sps, int differential, double refine_max_error_db, size_t refine_samples_per_symbol, double refine_design_margin_db, size_t refine_n_fft, size_t refine_zero_pad, bool refine_sequential, size_t refine_max_n_blocks, double carrier_freq_hz, double lost_confirm_s) Create an AsyncDsssReceiver in the searching state. |
| async_dsss_receiver_state_t * | async_dsss_receiver_create_handoff (const uint8_t * code, size_t code_len, double chip_rate, double symbol_rate, size_t spc, int m, double cn0_dbhz, double pfa, double pd, size_t segments, size_t sps, int differential, double refine_max_error_db, size_t refine_samples_per_symbol, double refine_design_margin_db, size_t refine_n_fft, size_t refine_zero_pad, bool refine_sequential, size_t refine_max_n_blocks, double carrier_freq_hz, double lost_confirm_s) Create a receiver in hand-off mode: idle, with no search of its own. |
| void | async_dsss_receiver_destroy (async_dsss_receiver_state_t * state) Destroy a receiver and release every child. |
| double | async_dsss_receiver_get_car_last_error (const 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 | async_dsss_receiver_get_car_nco_freq (const 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 | async_dsss_receiver_get_chip_phase (const async_dsss_receiver_state_t * state) |
| double | async_dsss_receiver_get_cn0_dbhz_est (const async_dsss_receiver_state_t * state) |
| int | async_dsss_receiver_get_code_locked (const 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 | async_dsss_receiver_get_code_rate (const async_dsss_receiver_state_t * state) |
| double | async_dsss_receiver_get_doppler_hz (const async_dsss_receiver_state_t * state) |
| int | async_dsss_receiver_get_idle (const async_dsss_receiver_state_t * state) 1 while waiting for a seed (hand-off mode, before seed() or after reset()); 0 in every other state. |
| double | async_dsss_receiver_get_lock (const async_dsss_receiver_state_t * state) |
| double | async_dsss_receiver_get_lock_metric (const 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 | async_dsss_receiver_get_lock_threshold (const async_dsss_receiver_state_t * state) The lock-metric declare threshold locked latches above (the lockdet up_thresh); exposed alongside lock_metric for engineering debug. |
| int | async_dsss_receiver_get_locked (const async_dsss_receiver_state_t * state) Binary carrier-lock flag from the loop's hysteretic (up/down verify-counted) lock detector — the de-chattered lock indicator, unlike the raw lock metric. |
| int | async_dsss_receiver_get_lost (const 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; the holder releases the assignment and calls reset(). 0 in every other state, and always 0 withlost_confirm_s = 0 . |
| double | async_dsss_receiver_get_mpsk_last_error (const async_dsss_receiver_state_t * state) MpskReceiver carrier phase discriminator (rad): the residual carrier phase LOOP 2 (post-despread) is not nulling. |
| int | async_dsss_receiver_get_n (const async_dsss_receiver_state_t * state) |
| double | async_dsss_receiver_get_nco_freq (const async_dsss_receiver_state_t * state) Live carrier loop-filter output = NCO frequency command (cycles/sample of the MpskReceiver output rate). Its mean tracks a Doppler ramp with no lag (unlike get_norm_freq's integrator estimate); its variance is the carrier loop stress. |
| double | async_dsss_receiver_get_norm_freq (const async_dsss_receiver_state_t * state) |
| int | async_dsss_receiver_get_refining (const async_dsss_receiver_state_t * state) |
| size_t | async_dsss_receiver_get_segments (const async_dsss_receiver_state_t * state) |
| size_t | async_dsss_receiver_get_sps (const async_dsss_receiver_state_t * state) |
| void | async_dsss_receiver_get_state (const async_dsss_receiver_state_t * state, void * blob) |
| int | async_dsss_receiver_get_tracking (const async_dsss_receiver_state_t * state) |
| void | async_dsss_receiver_reset (async_dsss_receiver_state_t * state) Return to the searching state _or, in hand-off mode, to idle. Resets the embedded Acquisition (if any) and rebuilds both the refine-stage and live-tracking chains 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 — matching every other object's reset() semantics in this codebase. In hand-off mode there is no search to return to, so this is how the holder of a pool releases a lost receiver for its next seed, with no reallocation._ | |
| int | async_dsss_receiver_seed (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 | async_dsss_receiver_set_refine_min_blocks (async_dsss_receiver_state_t * state, size_t n_blocks) Floor the refine's dwell at n_blocks , whatever the detection sizing asks (design section 12.16, #1265). |
| int | async_dsss_receiver_set_state (async_dsss_receiver_state_t * state, const void * blob) |
| size_t | async_dsss_receiver_state_bytes (const async_dsss_receiver_state_t * state) |
| async_dsss_receiver_status_t | async_dsss_receiver_status (const async_dsss_receiver_state_t * state) Read the status record (see async_dsss_receiver_status_t ). |
| size_t | async_dsss_receiver_steps (async_dsss_receiver_state_t * state, const float _Complex * x, size_t x_len, float _Complex * out, size_t max_out) Stream raw cf32 samples; emit demodulated symbols once tracking. |
| size_t | async_dsss_receiver_steps_max_out (async_dsss_receiver_state_t * state) |
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 3u |
| define | ASYNC_DSSS_RX_BN_CARRIER 0.04 |
| define | ASYNC_DSSS_RX_DLL_BN 0.002 |
| define | ASYNC_DSSS_RX_IDLE 3 |
| 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_LOST 4 |
| define | ASYNC_DSSS_RX_REFINE_MIN_BLOCKS 7u |
| define | ASYNC_DSSS_RX_REFINING 1 |
| define | ASYNC_DSSS_RX_SEARCHING 0 |
| define | ASYNC_DSSS_RX_TRACKING 2 |
Detailed Description¶
The production C port of the validated Python prototype's own search -> refine -> track pipeline (validated in the coupled-despreader, freq-refine, and end-to-end acquisition prototypes). Unlike DsssReceiver (which goes straight from an acquisition hit to tracking with the hit's own coarse Doppler estimate), this object inserts a REFINING stage between the two, closing a low-Es/N0 pull-in gap the coarse-only estimate leaves at large static Doppler offsets:
- searching (
get_tracking() == 0 && get_refining() == 0): samples feed the embeddedAcquisition. On a hit,acq_build_handoff()seeds the refine-stage chain (a FROZEN carrier derotationcostas_wipeoff()at the coarse estimate,costas_update()never called, the direct C equivalent of the Python prototype'sfreeze_carrier=Truefeeding a collectionDllwhosedll_lookback_segments(refine_max_error_db)windows OVERSAMPLE each epoch with coherent integrate-and-dump dumps the asynchronous data's residual carrier rides a ~symbol_rate-wide spectrum that a single per-epoch dump would undersample and alias (see therefine_max_error_dbdoc comment onasync_dsss_receiver_create()), then aRateConvertertoCarrierAcquisition's own operating rate, thenCarrierAcquisitionitself), and the unconsumed tail of the same call is handed straight to it. - refining (
get_refining() == 1): samples feed the refine-stage chain. Every call,CarrierAcquisition's ownready/give-up state is checked; once either fires, the live tracking chain is built FRESH (mirroring the already-learned "rebuild fresh, don't nudge in place" lesson) seeded from the ORIGINAL handoff chip phase (not wherever the refine-stageDlldrifted to) and the refined (or, on a give-up, unrefined) Doppler estimate and the object transitions to tracking. - tracking (
get_tracking() == 1): the refined carrier estimate is UNFROZEN into a live pre-despread carrier loop (costas_wipeoff/costas_update) ->Dll->RateConverter->MpskReceiverthe "track" leg of coarse -> freeze -> refine -> unfreeze/track.costas_update()runs once per code period, driven by 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 async ratio, so a transition lands inside nearly every period, and squaring is what makes the carrier error transition-robust (a decision-directed sign-aligned combine, tried first, thrashed +/-57deg and averaged to zero, so loop 1 never tracked and the post-despread MpskReceiver loop silently inherited the whole carrier + its Type-II ramp phase error). With that clean error and a bandwidth wide enough to pull the refined seed in and ride the ramp (ASYNC_DSSS_RX_BN_CARRIER), the pre-despread loop removes the FULL coupled Doppler (offset AND 500 Hz/s ramp), so despreading is coherent and MpskReceiver is left only a small residual. (Pure PLL no FLL anywhere, see theASYNC_DSSS_RX_BN_CARRIERcomment.) - idle (
get_idle() == 1, hand-off mode only): waiting for a seed. Samples are consumed and discarded, so a feeding loop needs no special case. - lost (
get_lost() == 1): the emitter is gone. Entered from tracking when BOTH lock flags have been down, without a break, for longer thanlost_confirm_s(docs/design/async-dsss-receiver.md section 11.2); the loops stop updating and samples are discarded untilreset(). One flag down is a degrade, reported by the flags and not acted on.lost_confirm_s = 0(the searching flavor's default) never enters it.
Hand-off mode (async_dsss_receiver_create_handoff()) is the same object with NO embedded Acquisition: the search is somebody else's a searcher covering one channel for every emitter on it and the receiver takes its detection from outside through async_dsss_receiver_seed(), exactly the record its own hit would have produced. It starts idle, reset() returns it to idle, and the searching branch of steps() is unreachable. seed() is a method of BOTH flavors (a hit is a seed the object made for itself), and it refuses on a receiver that already holds one: "assigned once" is enforced here, not by the caller's discipline.
Both 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) since they never run concurrently.
async_dsss_receiver_state_t *rx = 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)
float _Complex syms[4096];
size_t n = async_dsss_receiver_steps(rx, x, x_len, syms, 4096);
async_dsss_receiver_destroy(rx);
Public Functions Documentation¶
function 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 sample-rate/carrier-arm parameters) 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 so a failed pin leaves the receiver usable on its prior grid.
int async_dsss_receiver_configure_chain_raw (
async_dsss_receiver_state_t * state,
size_t segments,
size_t sps,
int n
)
Parameters:
stateMust be non-NULL.segmentsLive-tracking Dll segments per code period.spsMpskReceiver samples per symbol (the resample target).nMpskReceiver's carrier-arm count; must dividesps.
Returns:
0 on success, -1 on invalid grid or an allocation failure (the receiver is left usable at its prior grid on failure).
>>> import numpy as np
>>> from doppler.dsss import 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 async_dsss_receiver_configure_lock_raw¶
Re-tune the live-tracking Dll's code-lock detector directly. Forwards to dll_configure_lock_raw() on the live tracking Dll (the one behindget_code_locked() ), NOT the refine-stage collection Dll. Only meaningful once tracking has begun; a no-op while searching or refining. 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 async_dsss_receiver_configure_lock_raw (
async_dsss_receiver_state_t * state,
double up_thresh,
double down_thresh,
size_t n_looks,
double alpha,
uint32_t n_up,
uint32_t n_down
)
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 # a no-op until tracking begins 0
function async_dsss_receiver_configure_search_raw¶
Pin the embedded Acquisition's search grid directly. Forwards to acq_configure_search_raw() — the escape hatch under this object'ssymbol_rate -driven auto-sizing, for a power user who wants a specific(doppler_bins, n_noncoh) . Only meaningful while searching; the acquisition search does not run again until the nextreset() .
int async_dsss_receiver_configure_search_raw (
async_dsss_receiver_state_t * state,
size_t doppler_bins,
size_t n_noncoh
)
Parameters:
stateMust be non-NULL.doppler_binsNumber of Doppler window tiles to search (>= 1); capped by the create-timedoppler_uncertaintyspan (one tile per code-epoch Doppler bin width).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 acq_configure_search_raw) or in hand-off mode, which has no search to pin.
>>> 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 async_dsss_receiver_create¶
Create an AsyncDsssReceiver in the searching state.
async_dsss_receiver_state_t * async_dsss_receiver_create (
const uint8_t * code,
size_t code_len,
double chip_rate,
double symbol_rate,
size_t spc,
int m,
double cn0_dbhz,
double pfa,
double pd,
double doppler_uncertainty,
size_t segments,
size_t sps,
int differential,
double refine_max_error_db,
size_t refine_samples_per_symbol,
double refine_design_margin_db,
size_t refine_n_fft,
size_t refine_zero_pad,
bool refine_sequential,
size_t refine_max_n_blocks,
double carrier_freq_hz,
double lost_confirm_s
)
Only code/chip_rate/symbol_rate describe the signal itself. refine_* parameters mirror freq_refine.refine_seed_carrier_acq()'s own already-validated defaults (see objects/async_dsss_receiver.toml for the rationale behind each one) a power user can override, but the defaults are sized to close SPEC's own 4-5dB/500Hz-s combined gating scenario as-is.
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. Required.symbol_rateData-symbol rate, Hz. Required.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).refine_max_error_dbMax tolerable async-lookback correlation-power loss driving the refine-stage collection Dll's coherent-I&D window count via 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 own sequential mode; default false sequential mode's early per-block test fires on far too little averaging at SPEC's own Es/N0 floor (confirmed: as few as 4 blocks, 150-200+ Hz off); false waits the full design_snr-derived dwell_target, matching freq_refine.refine_seed_ carrier_acq()'s own validated default.refine_max_n_blocksCarrierAcquisition's own give-up cap in sequential 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 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.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 searching flavor's exit is reset(), as before.>>> 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 async_dsss_receiver_create_handoff¶
Create a receiver in hand-off mode: idle, with no search of its own.
async_dsss_receiver_state_t * async_dsss_receiver_create_handoff (
const uint8_t * code,
size_t code_len,
double chip_rate,
double symbol_rate,
size_t spc,
int m,
double cn0_dbhz,
double pfa,
double pd,
size_t segments,
size_t sps,
int differential,
double refine_max_error_db,
size_t refine_samples_per_symbol,
double refine_design_margin_db,
size_t refine_n_fft,
size_t refine_zero_pad,
bool refine_sequential,
size_t refine_max_n_blocks,
double carrier_freq_hz,
double lost_confirm_s
)
The pool shape of docs/design/async-dsss-receiver.md section 11.1: one searcher finds every emitter on the channel, and one of these per emitter tracks it from the searcher's detection. No Acquisition is built (a 20-to-50-tile engine per receiver, a dozen times over, is memory and work nothing would use), so there is no doppler_uncertainty and configure_search_raw() returns -1. The receiver starts idle and consumes samples without effect until async_dsss_receiver_seed() gives it a detection, after which the refine -> track chain is the searching flavor's, verbatim.
Every parameter is async_dsss_receiver_create()'s, minus the search half-range; pfa/pd still size CarrierAcquisition. The one default that differs is lost_confirm_s: 2.0 s, so an emitter that leaves is reported gone (get_lost()) and the holder can release the receiver against 5-to-15-minute on-times, two seconds past the measured fades costs nothing (section 12.3).
Parameters:
codeSpreading code, 0/1 chips (see async_dsss_receiver_create()).code_lenChips incode.chip_rateChip rate, Hz. Required.symbol_rateData-symbol rate, Hz. Required.spcSamples/chip; default 2.mPSK order, 2/4/8; default 2.cn0_dbhzDesign C/N0, dB-Hz; default 55.0 (derated byrefine_design_margin_dbinto CarrierAcquisition's design_snr).pfaCarrierAcquisition's false-alarm target; default 1e-3.pdCarrierAcquisition's detection target; default 0.9.segmentsLive-tracking Dll's segments; default 4.spsMpskReceiver's samples/symbol; default 8.differentialMpskReceiver's differential demap; default 0.refine_max_error_dbAs async_dsss_receiver_create().refine_samples_per_symbolAs async_dsss_receiver_create().refine_design_margin_dbAs async_dsss_receiver_create().refine_n_fftAs async_dsss_receiver_create().refine_zero_padAs async_dsss_receiver_create().refine_sequentialAs async_dsss_receiver_create().refine_max_n_blocksAs async_dsss_receiver_create().carrier_freq_hzNominal RF carrier for carrier->code aiding; 0.0 (default) = off.lost_confirm_sRelease rule, seconds of both flags down; default 2.0. 0 = never lost.>>> import numpy as np >>> from doppler.dsss import Acquisition, HandoffAsyncDsssReceiver >>> from doppler.dsss import bin_to_signed >>> from doppler.dsss.handoff import dll_init_chip_from_acq >>> 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 >>> 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)) The search is a separate object -- in a pool, one searcher per channel serves every receiver on it. Its hit is a correlation lag and a Doppler bin; the two documented helpers turn those into the seed: >>> acq = Acquisition(code, spc=spc, chip_rate=chip, symbol_rate=sym, ... cn0_dbhz=cn0, doppler_uncertainty=500.0) >>> for p in range(0, len(x) - te, te): ... hits = acq.push(x[p:p + te]) ... if hits: ... break >>> d_bin, lag, _, _, _, cn0_est, consumed = hits[0] >>> chip_phase = dll_init_chip_from_acq(lag, spc, sf) >>> res_hz = acq.doppler_res_hz >>> doppler_hz = bin_to_signed(d_bin, acq.doppler_bins) * res_hz The receiver never searched: it waits idle, takes the seed, and the samples from the hit onwards go to it. >>> rx = HandoffAsyncDsssReceiver( ... code, chip_rate=chip, symbol_rate=sym, spc=spc, cn0_dbhz=cn0) >>> rx.idle 1 >>> rx.seed(chip_phase, doppler_hz, cn0_est) >>> (rx.idle, rx.refining) (0, 1) >>> syms = [rx.steps(x[p:p + te]) ... for p in range(int(consumed), len(x) - te, te)] >>> syms = np.concatenate([s for s in syms if len(s)]) >>> rx.tracking # refined and tracking, no search 1 >>> len(syms) > 300 True >>> bool(np.mean(syms.real**2) > 10 * np.mean(syms.imag**2)) True Assigned once: a second seed is refused until reset(), which in this mode returns to idle, not to searching. >>> rx.seed(0.0, 0.0, cn0) # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError: seed refused: ... >>> rx.reset() >>> rx.idle 1
function async_dsss_receiver_destroy¶
Destroy a receiver and release every child.
Parameters:
stateMay be NULL.
function 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 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 async_dsss_receiver_get_chip_phase¶
function async_dsss_receiver_get_cn0_dbhz_est¶
function 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 async_dsss_receiver_get_code_rate¶
function async_dsss_receiver_get_doppler_hz¶
function async_dsss_receiver_get_idle¶
1 while waiting for a seed (hand-off mode, before seed() or after reset()); 0 in every other state.
function async_dsss_receiver_get_lock¶
function 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 async_dsss_receiver_get_lock_threshold¶
The lock-metric declare threshold locked latches above (the lockdet up_thresh); exposed alongside lock_metric for engineering debug.
function async_dsss_receiver_get_locked¶
Binary carrier-lock flag from the loop's hysteretic (up/down verify-counted) lock detector — the de-chattered lock indicator, unlike the raw lock metric.
function 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; the holder releases the assignment and calls reset(). 0 in every other state, and always 0 withlost_confirm_s = 0 .
function async_dsss_receiver_get_mpsk_last_error¶
MpskReceiver carrier phase discriminator (rad): the residual carrier phase LOOP 2 (post-despread) is not nulling.
function async_dsss_receiver_get_n¶
function async_dsss_receiver_get_nco_freq¶
Live carrier loop-filter output = NCO frequency command (cycles/sample of the MpskReceiver output rate). Its mean tracks a Doppler ramp with no lag (unlike get_norm_freq's integrator estimate); its variance is the carrier loop stress.
function async_dsss_receiver_get_norm_freq¶
function async_dsss_receiver_get_refining¶
function async_dsss_receiver_get_segments¶
function async_dsss_receiver_get_sps¶
function async_dsss_receiver_get_state¶
function async_dsss_receiver_get_tracking¶
function async_dsss_receiver_reset¶
Return to the searching state or, in hand-off mode, to idle. Resets the embedded Acquisition (if any) and rebuilds both the refine-stage and live-tracking chains 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 — matching every other object's reset() semantics in this codebase. In hand-off mode there is no search to return to, so this is how the holder of a pool releases a lost receiver for its next seed, with no reallocation.
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 async_dsss_receiver_seed¶
Take a detection from outside and start refining from it.
int async_dsss_receiver_seed (
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 section 11.1: 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 (the searching flavor's 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; the Python-side conversion from a lag is doppler.dsss.handoff. 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 idle (hand-off mode) or searching (the searching flavor: an outside hit simply beats its own). Also refused for a chip_phase outside [0, code_len) or a non-finite value.
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() until tracking refreshes it.
Returns:
DP_OK, or DP_ERR_INVALID when refused.
>>> import numpy as np
>>> from doppler.dsss import HandoffAsyncDsssReceiver
>>> from doppler.wfm import Gold
>>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
>>> rx = HandoffAsyncDsssReceiver(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.idle, rx.refining, rx.doppler_hz, rx.cn0_dbhz_est)
(0, 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 async_dsss_receiver_set_refine_min_blocks¶
Floor the refine's dwell at n_blocks , whatever the detection sizing asks (design section 12.16, #1265).
int async_dsss_receiver_set_refine_min_blocks (
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 section 12.10's floor dwell) hold the estimate to 77 Hz. Applied to the next refine chain built a receiver already refining keeps its dwell. Config, not running state: not in the blob. 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 async_dsss_receiver_set_state¶
function async_dsss_receiver_state_bytes¶
function async_dsss_receiver_status¶
Read the status record (see async_dsss_receiver_status_t ).
async_dsss_receiver_status_t async_dsss_receiver_status (
const async_dsss_receiver_state_t * state
)
Cheap and allocation-free: every field is a read of live state. The one-at-a-time getters below report the same fields; this is the face a pool holder uses.
Parameters:
stateMust be non-NULL.
Returns:
The record, by value.
>>> import numpy as np
>>> from doppler.dsss import HandoffAsyncDsssReceiver
>>> from doppler.wfm import Gold
>>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
>>> rx = HandoffAsyncDsssReceiver(code, chip_rate=3.069e6,
... symbol_rate=2700.0, spc=2)
>>> st = rx.status()
>>> (st.state, st.doppler_hz, st.code_locked, st.locked) # idle
(3, 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 async_dsss_receiver_steps¶
Stream raw cf32 samples; emit demodulated symbols once tracking.
size_t async_dsss_receiver_steps (
async_dsss_receiver_state_t * state,
const float _Complex * x,
size_t x_len,
float _Complex * out,
size_t max_out
)
Drives the search -> refine -> track state machine. While searching or refining, nothing is emitted (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. Accepts any block size; state carries across calls, so a capture can be fed in frames of any length with no seam. Idle (hand-off mode, before a seed) and lost (after the release rule fires) consume 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 (see lost_confirm_s). 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.
Returns:
Number of symbols written (0 while searching/refining, or while tracking with not yet 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 async_dsss_receiver_steps_max_out¶
Macro Definition Documentation¶
define ASYNC_DSSS_RECEIVER_STATE_MAGIC¶
define ASYNC_DSSS_RECEIVER_STATE_VERSION¶
define ASYNC_DSSS_RX_BN_CARRIER¶
define ASYNC_DSSS_RX_DLL_BN¶
define ASYNC_DSSS_RX_IDLE¶
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_LOST¶
define ASYNC_DSSS_RX_REFINE_MIN_BLOCKS¶
Default floor on the refine's dwell, blocks section 12.10's floor dwell (77 Hz of estimate noise at 45 dB-Hz); see async_dsss_receiver_set_refine_min_blocks().
define ASYNC_DSSS_RX_REFINING¶
define ASYNC_DSSS_RX_SEARCHING¶
define ASYNC_DSSS_RX_TRACKING¶
The documentation for this class was generated from the following file native/inc/async_dsss_receiver/async_dsss_receiver_core.h