File dsss_receiver_core.h¶
FileList > doppler > dsss_receiver > dsss_receiver_core.h
Go to the source code of this file
Composed continuous DSSS receiver: Acquisition -> Costas(bn_fll) pre-despread carrier wipeoff -> Dll(segments) -> RateConverter -> MpskReceiver, one object. More...
#include "doppler/RateConverter/RateConverter_core.h"#include "doppler/acq/acq_core.h"#include "doppler/cic/cic_core.h"#include "doppler/dll/dll_core.h"#include "doppler/dp_state.h"#include "doppler/hbdecim/hbdecim_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 <stddef.h>#include "doppler/costas/costas_core.h"#include "doppler/snr/snr_core.h"#include "doppler/ber/ber_core.h"
Classes¶
| Type | Name |
|---|---|
| struct | dp_dsss_receiver_state_t Composed receiver state. |
| struct | dsss_receiver_extra_t |
Public Functions¶
| Type | Name |
|---|---|
| int | dp_dsss_receiver_configure_chain_raw (dp_dsss_receiver_state_t * state, size_t segments, size_t sps, int n) Pin the despread/resample/demod grid directly, bypassing the create-time segments /sps defaults. |
| void | dp_dsss_receiver_configure_lock_raw (dp_dsss_receiver_state_t * state, double up_thresh, double down_thresh, size_t n_looks, double alpha, uint32_t n_up, uint32_t n_down) Re-tune the embedded Dll's code-lock detector directly. Forwards to dp_dll_configure_lock_raw() . Only meaningful once tracking has begun (dll is NULL before then); a no-op while searching. The detector is the hysteretic lockdet over the DLL's per-N-look CFAR statistic —up_thresh /down_thresh set the declare/drop levels andn_up /n_down the consecutive-look verify counts, trading declare latency against false-alarm rate. |
| int | dp_dsss_receiver_configure_search_raw (dp_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's ownsymbol_rate -driven auto-sizing, for a power user who wants a specific(doppler_bins, n_noncoh) instead. Only meaningful while searching (a no-op has already happened once tracking has begun; the acquisition search doesn't run again until the nextreset() ). |
| dp_dsss_receiver_state_t * | dp_dsss_receiver_create (const uint8_t * code, size_t code_len, double chip_rate, double symbol_rate, size_t spc, int m, double cn0_dbhz, double pfa, double pd, double doppler_uncertainty, size_t segments, size_t sps, int differential) Create a DSSS receiver in the searching state. |
| void | dp_dsss_receiver_destroy (dp_dsss_receiver_state_t * state) Destroy a receiver and release all four children. |
| double | dp_dsss_receiver_get_chip_phase (const dp_dsss_receiver_state_t * state) Dll's live tracked code phase (chips); 0.0 while searching. |
| double | dp_dsss_receiver_get_cn0_dbhz_est (const dp_dsss_receiver_state_t * state) |
| double | dp_dsss_receiver_get_code_rate (const dp_dsss_receiver_state_t * state) Dll's own tracking-quality indicator; 1.0 while searching. |
| double | dp_dsss_receiver_get_doppler_hz (const dp_dsss_receiver_state_t * state) |
| double | dp_dsss_receiver_get_lock (const dp_dsss_receiver_state_t * state) MpskReceiver's carrier lock EMA; 0.0 while searching. |
| int | dp_dsss_receiver_get_n (const dp_dsss_receiver_state_t * state) |
| double | dp_dsss_receiver_get_norm_freq (const dp_dsss_receiver_state_t * state) MpskReceiver's tracked carrier frequency; 0.0 while searching. |
| size_t | dp_dsss_receiver_get_segments (const dp_dsss_receiver_state_t * state) |
| size_t | dp_dsss_receiver_get_sps (const dp_dsss_receiver_state_t * state) |
| void | dp_dsss_receiver_get_state (const dp_dsss_receiver_state_t * state, void * blob) |
| int | dp_dsss_receiver_get_tracking (const dp_dsss_receiver_state_t * state) |
| void | dp_dsss_receiver_reset (dp_dsss_receiver_state_t * state) Return to the searching state. Resets the embedded Acquisition and frees dll /rc /rx (rebuilt from scratch on the next hit) — a receiver that has locked cannot be "reset back to tracking the same signal," only back to searching, matching every other object's reset() semantics in this codebase. |
| int | dp_dsss_receiver_set_state (dp_dsss_receiver_state_t * state, const void * blob) |
| size_t | dp_dsss_receiver_state_bytes (const dp_dsss_receiver_state_t * state) |
| size_t | dp_dsss_receiver_steps (dp_dsss_receiver_state_t * state, const float _Complex * x, size_t x_len, float _Complex * out, size_t max_out) Stream raw cf32 samples; emit demodulated symbols once locked. |
| size_t | dp_dsss_receiver_steps_max_out (dp_dsss_receiver_state_t * state) |
Macros¶
| Type | Name |
|---|---|
| define | DSSS_RECEIVER_STATE_MAGIC [**DP\_FOURCC**](dp__state_8h.md#define-dp_fourcc) ('D', 'S', 'R', 'X') |
| define | DSSS_RECEIVER_STATE_VERSION /* multi line expression */ |
| define | DSSS_RX_BN_CARRIER 0.01 |
| define | DSSS_RX_BN_FLL 0.03 |
Detailed Description¶
The single-object form of the chain validated across this repo's "continuous async-DSSS receiver" story (docs/gallery/async-dsss-receiver-spec.md, docs/gallery/dsss-acq-characterization.md, docs/gallery/dsss-receiver.md): a continuous, non-bursty spreading code whose data-symbol clock need not be synchronous to the code-epoch clock. steps() streams raw samples through whichever child is currently active:
- searching (
tracking() == 0): samples feed the embeddedAcquisition. Nothing is emitted. On a hit, the carrier loop/Dll/RateConverter/MpskReceiverare built from the hit's code phase and Doppler estimate (the exactacq_result_t::chip_phasehand-off phase andRateConverter-bridged sample-rate hand-off this repo's gallery pages validated by hand), and the unconsumed tail of the samesteps()call is handed straight to them — no samples are dropped at the transition. - tracking (
tracking() == 1): samples are first derotated by a pre-despread carrier loop (costas_wipeoff/costas_update, one update per code period,bn_fll-assisted removes BULK Doppler and its RATE OF CHANGE before the code loop ever sees it; a fixed/ bounded residual alone is fine downstream-only, perdocs/design/async-dsss-receiver.md§3.4, but an unbounded Doppler RATE is not), one code period at a time (a small internal carry buffer holds any leftover partial-period tail across callssteps()still accepts any block size), then feedDll -> RateConverter -> MpskReceiverin sequence the C-level equivalent of hand-composing those four objects (plus the new carrier stage) and demodulated symbols are emitted. This is a NEW composition living entirely in this object deliberately NOT a swap to the existingDespreaderobject (which fuses Costas+Dll per-sample), becauseDespreaderembedsDllviadp_dll_init(), hardcoded tosegments==1; it cannot carry this object's ownsegments>1async-lookback tracking.dp_dll_steps()itself is called completely unmodified.
Per [[feedback_despread_resample_demod_separation]] (this story's own hard-won lesson): segments (the despreader's own tracking parameter) and sps (the demodulator's own sample-rate need) are independently configurable and bridged by an explicit RateConverter, never coupled to each other.
// "Just works": only the signal's own physical parameters are required.
dp_dsss_receiver_state_t *rx = dp_dsss_receiver_create(
code, code_len, 3.0e6, 2100.0, // chip_rate, symbol_rate
2, 2, // spc, m (BPSK)
55.0, 1e-3, 0.9, 100.0, // cn0_dbhz, pfa, pd,
// doppler_uncertainty
4, 8, // segments, sps
0); // differential
float _Complex syms[4096];
size_t n = dp_dsss_receiver_steps(rx, x, x_len, syms, 4096);
dp_dsss_receiver_destroy(rx);
Public Functions Documentation¶
function dp_dsss_receiver_configure_chain_raw¶
Pin the despread/resample/demod grid directly, bypassing the create-time segments /sps defaults.
int dp_dsss_receiver_configure_chain_raw (
dp_dsss_receiver_state_t * state,
size_t segments,
size_t sps,
int n
)
The escape hatch for the one composition-specific knob this object adds beyond its children's own: segments (Dll's tracking parameter) and sps/n (MpskReceiver's samples and terminal outputs per symbol) are independently overridable here, still bridged by a freshly-sized RateConverter never coupled to each other (see the module docstring). It rebuilds dll/rc/rx at once, allocating every replacement before freeing the old ones (Acquisition's acq_regrid() discipline), and the pinned grid is the one every later chain is built on: the next hit's hand-off and reset() both rebuild at it.
The new grid is checked before anything is rebuilt, and refused with the receiver left on its prior grid: segments >= 1, sps >= 2, a symbol no faster than a chip and a resampling ratio that is a normal double (doppler#2103), and an n MpskReceiver would accept (doppler#2426). Still an abort: an allocation failure in the rebuild, which runs under dp_xnn (doppler#2112).
Parameters:
stateThe receiver.segmentsDll tracking segments per code period, >= 1.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 (the receiver keeps its prior grid).
>>> import numpy as np
>>> from doppler.dsss import DsssReceiver
>>> from doppler.wfm import Gold
>>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
>>> rx = DsssReceiver(code, chip_rate=3.0e6, symbol_rate=2100.0, spc=2)
>>> rx.configure_chain_raw(segments=6, sps=8, n=8) # re-pin the chain
>>> rx.segments # tracking grid updated in place
6
function dp_dsss_receiver_configure_lock_raw¶
Re-tune the embedded Dll's code-lock detector directly. Forwards to dp_dll_configure_lock_raw() . Only meaningful once tracking has begun (dll is NULL before then); a no-op while searching. The detector is the hysteretic lockdet over the DLL's per-N-look CFAR statistic —up_thresh /down_thresh set the declare/drop levels andn_up /n_down the consecutive-look verify counts, trading declare latency against false-alarm rate.
void dp_dsss_receiver_configure_lock_raw (
dp_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 DsssReceiver >>> from doppler.wfm import Gold >>> code = np.asarray(Gold().generate(1023)).astype(np.uint8) >>> rx = DsssReceiver(code, chip_rate=3.0e6, symbol_rate=2100.0, spc=2) >>> rx.configure_lock_raw(up_thresh=0.4, down_thresh=0.2, n_looks=20, ... alpha=0.1, n_up=5, n_down=3) >>> rx.tracking # a no-op until a hit builds the Dll 0
function dp_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's ownsymbol_rate -driven auto-sizing, for a power user who wants a specific(doppler_bins, n_noncoh) instead. Only meaningful while searching (a no-op has already happened once tracking has begun; the acquisition search doesn't run again until the nextreset() ).
int dp_dsss_receiver_configure_search_raw (
dp_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 dp_acq_configure_search_raw).
>>> import numpy as np
>>> from doppler.dsss import DsssReceiver
>>> from doppler.wfm import Gold
>>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
>>> rx = DsssReceiver(code, chip_rate=3.0e6, symbol_rate=2100.0, spc=2)
>>> rx.configure_search_raw(doppler_bins=1, n_noncoh=16) # pin it
>>> rx.tracking # still searching, on the pinned grid
0
function dp_dsss_receiver_create¶
Create a DSSS receiver in the searching state.
dp_dsss_receiver_state_t * dp_dsss_receiver_create (
const uint8_t * code,
size_t code_len,
double chip_rate,
double symbol_rate,
size_t spc,
int m,
double cn0_dbhz,
double pfa,
double pd,
double doppler_uncertainty,
size_t segments,
size_t sps,
int differential
)
Only code/chip_rate/symbol_rate describe the signal itself — everything else is a physically-motivated default a caller can override, not a requirement. Internally: the embedded Acquisition is built via dp_acq_create_continuous() (this receiver is inherently continuous/streaming) always window-tiles, never coherently combines across epochs, sensitivity purely from an internally auto-sized non-coherent look count (see acq_core.h's file doc comment); Dll always uses bn=0.002 (this story's own validated stable loop bandwidth for a one-update-per-code-epoch geometry, not dp_dll_create()'s own default of 0.01, which this story found unstable here) and zeta=0.707, spacing=0.5; MpskReceiver always uses pulse=iandd, bn_carrier=bn_timing=0.01, zeta=0.707 and lock_thresh=0.3 — this story's own validated values throughout. It also passed acq_to_track=1 and warmup_syms=30 until those were deleted (doppler#877, 1f417e97); the composed receiver now runs its one NDA discriminator here as everywhere else. lock_thresh=0.3 predates the lock statistic becoming a calibrated detector and is retained because it is validated here, but it now has a derivable reading: the carrier lock EMA's noise-only sd is 0.1132 at every M, so 0.3 is 2.65 noise sigmas, a per-look Pfa of ~4e-3 — looser than MpskReceiver's own 0.5 default (4.42 sigma, 5e-6) and still ~6 sigma clear of the +0.99 a locked BPSK constellation reads, which is why it holds. See carrier_nda_core.h. n (MpskReceiver's carrier-arm count) is derived from sps: the largest divisor of sps in {4, 2, 1}.
Parameters:
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(the spreading factor).chip_rateChip rate, Hz. Required.symbol_rateData-symbol rate, Hz. Required — passed straight to the embedded Acquisition's ownsymbol_rate(diagnostic there; seedp_acq_create_continuous()).spcSamples/chip (front-end oversample); default 2 (fs = 2x chip_rate).mPSK order, 2/4/8; default 2 (BPSK).cn0_dbhzDesign C/N0 for acquisition sizing, dB-Hz; default 55.0.pfaAcquisition false-alarm target; default 1e-3.pdAcquisition detection-probability target; default 0.9.doppler_uncertaintyOne-sided Doppler search half-range, Hz; default 100.0.segmentsDll's partial correlations per code epoch: its despread output rate and the grain of its look-back window, independent ofsps(see the module docstring); default 4.spsMpskReceiver's samples/symbol, reached by an internal RateConverter bridging the despreader's own partial rate to this rate; default 8, MpskReceiver's own constructor default.differentialMpskReceiver's differential (rotation- invariant) demap; default 0 (coherent).>>> import numpy as np >>> from doppler.dsss import DsssReceiver >>> from doppler.wfm import Gold >>> sf, chip, sym, spc = 1023, 3.0e6, 2100.0, 2 >>> fs, te, tsym = chip * spc, sf * spc, chip * spc / sym >>> code = np.asarray(Gold().generate(sf)).astype(np.uint8) >>> csign = np.where(code & 1, -1.0, 1.0) >>> rng = np.random.default_rng(6) >>> n = int(400 * tsym) + 2 * te # 400 BPSK data symbols >>> idx = np.arange(n) >>> data = (rng.integers(0, 2, 404) * 2 - 1).astype(float) >>> si = np.clip((idx / tsym).astype(int), 0, 403) >>> spread = data[si] * csign[(idx // spc) % sf] # DSSS chips >>> sig = spread * np.exp(2j * np.pi * (50.0 / fs) * idx) # +50 Hz >>> pre = 3 * te # noise-only lead-in, pre-signal >>> sigma = np.sqrt(fs / 10 ** (90.0 / 10)) # ~90 dB-Hz C/N0 >>> noise = (sigma / np.sqrt(2)) * (rng.standard_normal(pre + n) ... + 1j * rng.standard_normal(pre + n)) >>> x = (np.concatenate([np.zeros(pre), sig]).astype(np.complex64) ... + noise.astype(np.complex64)) >>> rx = DsssReceiver(code, chip_rate=chip, symbol_rate=sym, spc=spc, ... cn0_dbhz=55.0, doppler_uncertainty=100.0) >>> syms = [rx.steps(x[p:p + te]) for p in range(0, len(x) - te, te)] >>> syms = np.concatenate([s for s in syms if len(s)]) >>> rx.tracking # acquired, now demodulating 1 >>> len(syms) > 300 # a few hundred symbols recovered True Nearly all the energy lands on I, so the BPSK phase is resolved: >>> bool(np.mean(syms.real**2) > 10 * np.mean(syms.imag**2)) True
function dp_dsss_receiver_destroy¶
Destroy a receiver and release all four children.
Parameters:
stateMay be NULL.
function dp_dsss_receiver_get_chip_phase¶
Dll's live tracked code phase (chips); 0.0 while searching.
function dp_dsss_receiver_get_cn0_dbhz_est¶
function dp_dsss_receiver_get_code_rate¶
Dll's own tracking-quality indicator; 1.0 while searching.
function dp_dsss_receiver_get_doppler_hz¶
function dp_dsss_receiver_get_lock¶
MpskReceiver's carrier lock EMA; 0.0 while searching.
function dp_dsss_receiver_get_n¶
function dp_dsss_receiver_get_norm_freq¶
MpskReceiver's tracked carrier frequency; 0.0 while searching.
function dp_dsss_receiver_get_segments¶
function dp_dsss_receiver_get_sps¶
function dp_dsss_receiver_get_state¶
function dp_dsss_receiver_get_tracking¶
function dp_dsss_receiver_reset¶
Return to the searching state. Resets the embedded Acquisition and frees dll /rc /rx (rebuilt from scratch on the next hit) — a receiver that has locked cannot be "reset back to tracking the same signal," only back to searching, matching every other object's reset() semantics in this codebase.
Parameters:
stateMust be non-NULL.>>> import numpy as np >>> from doppler.dsss import DsssReceiver >>> from doppler.wfm import Gold >>> code = np.asarray(Gold().generate(1023)).astype(np.uint8) >>> rx = DsssReceiver(code, chip_rate=3.0e6, symbol_rate=2100.0, spc=2) >>> rx.reset() # abort any lock, hunt from scratch >>> (rx.tracking, rx.chip_phase) # back to searching, all cleared (0, 0.0)
function dp_dsss_receiver_set_state¶
function dp_dsss_receiver_state_bytes¶
function dp_dsss_receiver_steps¶
Stream raw cf32 samples; emit demodulated symbols once locked.
size_t dp_dsss_receiver_steps (
dp_dsss_receiver_state_t * state,
const float _Complex * x,
size_t x_len,
float _Complex * out,
size_t max_out
)
While searching, samples feed the embedded Acquisition and nothing is emitted (0 return is normal, not an error). The moment a hit fires, Dll/RateConverter/MpskReceiver are built and seeded from it, and the rest of THIS call from where the hit's frame ended — the hit's own samples_consumed, so no samples are dropped or double-fed — is handed straight to them in the same call. While tracking, samples feed Dll -> RateConverter -> MpskReceiver in sequence. Accepts any block size; state carries across calls (Acquisition/Dll/ RateConverter/MpskReceiver are all already block-size invariant, so this object needs no ring-buffering of its own).
Parameters:
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, or while tracking with not yet a full symbol's worth of input).
>>> import numpy as np
>>> from doppler.dsss import DsssReceiver
>>> from doppler.wfm import Gold
>>> sf, chip, sym, spc = 1023, 3.0e6, 2100.0, 2
>>> fs, te, tsym = chip * spc, sf * spc, chip * spc / sym
>>> code = np.asarray(Gold().generate(sf)).astype(np.uint8)
>>> csign = np.where(code & 1, -1.0, 1.0)
>>> rng = np.random.default_rng(6)
>>> n = int(400 * tsym) + 2 * te # 400 BPSK data symbols
>>> idx = np.arange(n)
>>> data = (rng.integers(0, 2, 404) * 2 - 1).astype(float)
>>> si = np.clip((idx / tsym).astype(int), 0, 403)
>>> spread = data[si] * csign[(idx // spc) % sf] # DSSS chips
>>> sig = spread * np.exp(2j * np.pi * (50.0 / fs) * idx) # +50 Hz
>>> pre = 3 * te # noise-only lead-in, pre-signal
>>> sigma = np.sqrt(fs / 10 ** (90.0 / 10)) # ~90 dB-Hz C/N0
>>> noise = (sigma / np.sqrt(2)) * (rng.standard_normal(pre + n)
... + 1j * rng.standard_normal(pre + n))
>>> x = (np.concatenate([np.zeros(pre), sig]).astype(np.complex64)
... + noise.astype(np.complex64))
>>> rx = DsssReceiver(code, chip_rate=chip, symbol_rate=sym, spc=spc,
... cn0_dbhz=55.0, doppler_uncertainty=100.0)
>>> syms = [rx.steps(x[p:p + te]) for p in range(0, len(x) - te, te)]
>>> syms = np.concatenate([s for s in syms if len(s)])
>>> rx.tracking # acquired and now demodulating
1
>>> len(syms) > 300 # a few hundred symbols recovered
True
Nearly all the energy lands on I, so the BPSK phase is resolved:
>>> bool(np.mean(syms.real**2) > 10 * np.mean(syms.imag**2))
True
function dp_dsss_receiver_steps_max_out¶
Macro Definition Documentation¶
define DSSS_RECEIVER_STATE_MAGIC¶
define DSSS_RECEIVER_STATE_VERSION¶
define DSSS_RX_BN_CARRIER¶
define DSSS_RX_BN_FLL¶
The documentation for this class was generated from the following file native/inc/doppler/dsss_receiver/dsss_receiver_core.h