File burst_demod_core.h¶
FileList > burst_demod > burst_demod_core.h
Go to the source code of this file
Feedforward BPSK DSSS frame demodulator. More...
#include "clib_common.h"#include "jm_perf.h"#include "ppe/ppe_core.h"#include "fft/fft_core.h"#include "spectral/spectral_core.h"#include <complex.h>#include "conv/conv_core.h"#include "rs/rs_core.h"#include "pn/pn_core.h"#include "gold/gold_core.h"#include "mpsk/mpsk_core.h"
Classes¶
| Type | Name |
|---|---|
| struct | burst_demod_state_t BurstDemod state. Allocate with burst_demod_create() . |
Public Functions¶
| Type | Name |
|---|---|
| burst_demod_state_t * | burst_demod_create (const uint8_t * data_code, size_t data_code_len, size_t spc, double chip_rate, double carrier_hz, double max_rate, size_t frame_syms, size_t est_segments) Create a feedforward BPSK DSSS burst demodulator. |
| size_t | burst_demod_demod (burst_demod_state_t * state, const float _Complex * x, size_t x_len, uint8_t * out, size_t max_out) Demodulate one burst end to end and write the frame's bits. |
| size_t | burst_demod_demod_max_out (burst_demod_state_t * state) Max output bits = frame_syms (caller sizes the buffer). |
| void | burst_demod_destroy (burst_demod_state_t * state) Destroy a demodulator. |
| size_t | burst_demod_llrs (burst_demod_state_t * state, size_t n, float * out, size_t max_out) LLRs the last demod() wrote — the frame's soft bits. |
| size_t | burst_demod_llrs_max_out (burst_demod_state_t * state, size_t n) Max LLRs burst_demod_llrs() writes: the frame's length in bits. |
| void | burst_demod_reset (burst_demod_state_t * state) Clear the per-burst read-backs, leaving the configuration intact. |
| void | burst_demod_set_preamble (burst_demod_state_t * state, const uint8_t * acq_code, size_t acq_code_len, size_t reps) Register the unmodulated acquisition preamble code and its repetition count used for the feedforward (f0, rate) estimate. |
| void | burst_demod_set_prior (burst_demod_state_t * state, double f0_coarse, size_t start) Seed the demodulator from acquisition with the coarse Doppler and the preamble start sample. |
| void | burst_demod_set_sync (burst_demod_state_t * state, const uint8_t * sync, size_t sync_len) Register the known frame-sync word used for frame alignment and phase/sign resolution. |
| size_t | burst_demod_symbols (burst_demod_state_t * state, size_t n, float _Complex * out, size_t max_out) The last demod()'s DEROTATED complex symbols — the constellation the LLRs are the real part of. |
| size_t | burst_demod_symbols_max_out (burst_demod_state_t * state, size_t n) Max symbols burst_demod_symbols() writes: the frame's length. |
Detailed Description¶
The whole post-acquisition payload chain, in C, with no tracking loops:
* preamble estimate — segment-despread the unmodulated, repeated acq preamble into partial correlations and feed them to ppe, giving a coarse (frequency, chirp-rate);
* sample-rate dechirp by (f0, rate) — removes Doppler AND Doppler rate;
* despread the data section with the (short) data code -> soft BPSK symbols;
* frame sync — correlate the symbols against the known sync word; the complex peak gives the frame offset and the residual phase (derotated);
* slice frame_syms symbols to bits, hard and soft, and STOP.
Where this object's job ends¶
At a decision. It hands back one bit per symbol (demod()) and one LLR per symbol (burst_demod_llrs()), and it does not know what any of them mean: which are payload, which are a check, what an outer code would repair are all questions about a FRAME, and answering them needs a description this object deliberately does not hold (doppler#1022). It used to hold half of one — a hard-coded sync | payload | CRC-16 — which is how a burst sent without a trailer came to be reported invalid.
What it does need is the sync word, to find the frame and resolve the BPSK sign, and frame_syms, to know how many symbols to slice. Both are physical-layer facts.
Seed from acquisition with set_prior(coarse Doppler, preamble start), set_preamble(acq code, reps) and set_sync(sync word), then demod(burst). One max_rate knob spans near-static Doppler (0) to severe LEO chirp. One-shot per burst. Composes ppe (which composes fft + spectral).
burst_demod_state_t *d = burst_demod_create(dcode, 50, 4, 1e6, 0, 0, 256, 10);
burst_demod_set_preamble(d, acode, 500, 5);
burst_demod_set_sync(d, sync, 31);
burst_demod_set_prior(d, f0_coarse, preamble_start);
size_t nbits = burst_demod_demod(d, x, n, bits, 256); // frame bits out
Public Functions Documentation¶
function burst_demod_create¶
Create a feedforward BPSK DSSS burst demodulator.
burst_demod_state_t * burst_demod_create (
const uint8_t * data_code,
size_t data_code_len,
size_t spc,
double chip_rate,
double carrier_hz,
double max_rate,
size_t frame_syms,
size_t est_segments
)
Recovers the payload of a single spread burst end to end, with no tracking loops: it estimates the burst's Doppler (and Doppler rate) from the unmodulated acquisition preamble, dechirps by that estimate, despreads the data section into soft symbols, aligns on the known sync word, slices to bits, and checks the CRC-16 trailer. One max_rate knob spans the whole range from near-static Doppler (0) to a severe LEO chirp.
After construction, register the templates and the acquisition seed — set_preamble(), set_sync(), set_prior() — then call demod() once per burst.
Parameters:
data_codeData spreading code, one 0/1 chip per element; copied into the object (its length is the data spreading factor, chips/symbol).data_code_lenData spreading factor (chips/symbol); the length ofdata_code.spcSamples per chip (front-end oversample).chip_rateChip rate (Hz); sets the sample rate as spc*chip_rate.carrier_hzRF carrier (Hz) for code-Doppler scaling; 0 = ignore.max_rateChirp-rate search half-span (cycles/sample^2 at the input rate); 0 = Doppler only (no rate search).frame_symsSymbols the frame occupies after the sync word — how many bits demod() hands back per burst. What they mean is a frame description's business.est_segmentsPartial correlations per acq period (segmentation for the feedforward estimate; larger tolerates more rate).>>> import numpy as np >>> from doppler.dsss import BurstDemod >>> spc, acq_sf, reps, data_sf = 4, 500, 5, 50 >>> sync = np.array([0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0], np.uint8) >>> acode = ((np.arange(acq_sf) * 2654435761 >> 13) & 1).astype( ... np.uint8) >>> dcode = ((np.arange(data_sf) * 40503 >> 7) & 1).astype(np.uint8) >>> payload = ((np.arange(64) * 7 + 3) & 1).astype(np.uint8) >>> def crc16(bits): ... c = 0xFFFF ... for b in bits: ... c ^= (int(b) & 1) << 15 ... c = (((c << 1) ^ 0x1021) & 0xFFFF ... if c & 0x8000 else (c << 1) & 0xFFFF) ... return c >>> crc = crc16(payload) >>> crc_bits = np.array( ... [(crc >> (15 - j)) & 1 for j in range(16)], np.uint8) >>> frame = np.concatenate([sync, payload, crc_bits]) >>> csign = lambda b: np.where(np.asarray(b) & 1, -1.0, 1.0) >>> chips = ([np.tile(csign(acode), reps)] ... + [csign(b) * csign(dcode) for b in frame]) >>> bb = np.repeat(np.concatenate(chips), spc).astype(np.complex64) >>> n = np.arange(len(bb)) >>> f0 = 0.012 >>> x = (bb * np.exp(2j * np.pi * f0 * n)).astype(np.complex64) >>> d = BurstDemod(dcode, spc=spc, chip_rate=1e6, frame_syms=len(frame)) >>> d.set_preamble(acode, reps) # unmodulated (f0, rate) preamble >>> d.set_sync(sync) # Barker-13: frame align + sign fix >>> d.set_prior(f0, 0) # coarse Doppler + preamble start >>> bits = d.demod(x) # estimate -> dechirp -> despread -> slice >>> bool(np.array_equal(bits, frame)) # the FRAME, not the payload True
function burst_demod_demod¶
Demodulate one burst end to end and write the frame's bits.
size_t burst_demod_demod (
burst_demod_state_t * state,
const float _Complex * x,
size_t x_len,
uint8_t * out,
size_t max_out
)
Runs the whole feedforward chain on the supplied samples: estimate the (frequency, chirp-rate) from the preamble, dechirp, despread the data section to soft symbols, sync-align and derotate, and slice frame_syms symbols to bits. It writes the frame as received — sync word first — and makes no claim about what those bits are for: undoing the frame needs a description, and that is a caller's, not this object's. The soft twin of the same decisions is burst_demod_llrs().
On return the read-back fields report the outcome — frame_offset, n_symbols, and the est_freq_hz / est_rate_hz / est_snr_db estimates. The templates and prior must already be set via set_preamble(), set_sync(), set_prior().
The C function returns the number of bits written; the Python binding returns those bits as an array (a view into a reused buffer unless an out buffer is supplied).
Parameters:
stateDemodulator handle.xBurst samples (complex baseband at spc*chip_rate).x_lenNumber of input samples.outCaller-provided output buffer for the frame's bits.max_outCapacity ofout, in bits.
Returns:
Number of frame bits written (0 on failure / too-short burst).
>>> import numpy as np
>>> from doppler.dsss import BurstDemod
>>> spc, acq_sf, reps, data_sf = 4, 500, 5, 50
>>> sync = np.array([0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0], np.uint8)
>>> acode = ((np.arange(acq_sf) * 2654435761 >> 13) & 1).astype(
... np.uint8)
>>> dcode = ((np.arange(data_sf) * 40503 >> 7) & 1).astype(np.uint8)
>>> payload = ((np.arange(64) * 7 + 3) & 1).astype(np.uint8)
>>> def crc16(bits):
... c = 0xFFFF
... for b in bits:
... c ^= (int(b) & 1) << 15
... c = (((c << 1) ^ 0x1021) & 0xFFFF
... if c & 0x8000 else (c << 1) & 0xFFFF)
... return c
>>> crc = crc16(payload)
>>> crc_bits = np.array(
... [(crc >> (15 - j)) & 1 for j in range(16)], np.uint8)
>>> frame = np.concatenate([sync, payload, crc_bits])
>>> csign = lambda b: np.where(np.asarray(b) & 1, -1.0, 1.0)
>>> chips = ([np.tile(csign(acode), reps)]
... + [csign(b) * csign(dcode) for b in frame])
>>> bb = np.repeat(np.concatenate(chips), spc).astype(np.complex64)
>>> n = np.arange(len(bb))
>>> f0 = 0.012
>>> x = (bb * np.exp(2j * np.pi * f0 * n)).astype(np.complex64)
>>> d = BurstDemod(dcode, spc=spc, chip_rate=1e6, frame_syms=93)
>>> d.set_preamble(acode, reps)
>>> d.set_sync(sync)
>>> d.set_prior(f0, 0)
>>> bits = d.demod(x)
>>> bool(np.array_equal(bits, frame)) # sync | payload | CRC, as sent
True
>>> from doppler.wfm import crc16
>>> int(crc16(bits[13:77])) == crc # the CHECK is the caller's
True
function burst_demod_demod_max_out¶
Max output bits = frame_syms (caller sizes the buffer).
function burst_demod_destroy¶
Destroy a demodulator.
Parameters:
stateMay be NULL.
function burst_demod_llrs¶
LLRs the last demod() wrote — the frame's soft bits.
crealf(sym * derot) IS the log-likelihood ratio up to a scale, and it was computed, sliced to one bit and freed on every burst. A hard decision throws away roughly 2 dB of the coding gain a soft-input decoder exists to deliver (mpsk_soft_demap's own docstring), so this is what makes a coded burst worth coding.
The convention is not a new one: mpsk_soft_demap's, which is mpsk_demap's decision rule seen a second way. Positive means bit 0, so L < 0 reproduces exactly the bits demod() returned — asserted in the tests rather than assumed.
Spans the WHOLE frame, not just the payload, because a code covers what its description says it covers and a decoder needs the bits the code protects. The payload's own span is field_off/field_bits of the layout.
Scaled by est_n0 rather than left raw: a Viterbi is invariant to a positive scale, but LLRs from different bursts are not comparable without one, and combining across bursts needs them to be.
Parameters:
stateDemodulator handle.nIgnored — the count is the last demod()'s frame.outReceives the LLRs, one per frame bit.max_outCapacity ofout; see burst_demod_llrs_max_out().
Returns:
LLRs written — min(frame bits, max_out), or 0 if the last demod() produced no frame.
>>> import numpy as np
>>> from doppler.dsss import BurstDemod
>>> dcode = (np.arange(50) & 1).astype(np.uint8)
>>> d = BurstDemod(dcode, spc=4, chip_rate=1e6, frame_syms=93)
>>> d.set_sync(np.zeros(13, dtype=np.uint8))
>>> d.llrs_max_out(1) # one per frame symbol
93
function burst_demod_llrs_max_out¶
Max LLRs burst_demod_llrs() writes: the frame's length in bits.
Parameters:
stateDemodulator handle.nIgnored — the count is the last demod()'s frame.
function burst_demod_reset¶
Clear the per-burst read-backs, leaving the configuration intact.
Zeros the after-demod fields (frame_offset, n_symbols, and the est_* estimates) so a stale result cannot be mistaken for a fresh one. The spreading codes, sync word, and prior set up before the first burst are preserved, so the object is immediately ready to demodulate the next burst.
Parameters:
stateDemodulator handle.
function burst_demod_set_preamble¶
Register the unmodulated acquisition preamble code and its repetition count used for the feedforward (f0, rate) estimate.
void burst_demod_set_preamble (
burst_demod_state_t * state,
const uint8_t * acq_code,
size_t acq_code_len,
size_t reps
)
The preamble is the acq spreading code transmitted reps times with no data modulation; demod() segment-despreads it into partial correlations and feeds those to the polynomial-phase estimator to recover the coarse (frequency, chirp-rate). Call once after construction; the code is copied.
Parameters:
stateDemodulator handle.acq_codeAcq preamble spreading code, one 0/1 chip per element; copied into the object.acq_code_lenAcq code length (chips); the length ofacq_code.repsNumber of preamble repetitions in the burst.>>> import numpy as np >>> from doppler.dsss import BurstDemod >>> dcode = (np.arange(50) & 1).astype(np.uint8) >>> d = BurstDemod(dcode, spc=4, chip_rate=1e6, frame_syms=93) >>> acode = (np.arange(500) & 1).astype(np.uint8) # unmodulated >>> d.set_preamble(acode, reps=5) # 5 reps drive the (f0, rate) fit
function burst_demod_set_prior¶
Seed the demodulator from acquisition with the coarse Doppler and the preamble start sample.
These come from the upstream acquisition stage: f0_coarse centres the feedforward frequency search near the true Doppler, and start tells demod() where the preamble begins within the burst so it despreads the right samples. Call once per burst before demod().
Parameters:
stateDemodulator handle.f0_coarseCoarse Doppler prior (cycles/sample at the input rate).startPreamble start sample index within the burst.
function burst_demod_set_sync¶
Register the known frame-sync word used for frame alignment and phase/sign resolution.
After the data section is despread to soft BPSK symbols, demod() correlates them against this word; the complex correlation peak locates the frame (its frame_offset) and its phase resolves the residual carrier rotation and the BPSK sign ambiguity before slicing. Pass the word as 0/1 symbols; it is copied and stored internally as +/-1.
This is the ONLY thing this object is told about the frame's content, and it is told it for a physical-layer reason: without the sign the slicer would be a coin toss. Everything else — where the payload sits, which stages cover what, whether a check passed — needs the frame's description and belongs one layer up (doppler#1022).
Parameters:
stateDemodulator handle.syncFrame-sync word, one 0/1 symbol per element; copied.sync_lenSync word length (symbols); the length ofsync.>>> import numpy as np >>> from doppler.dsss import BurstDemod >>> dcode = (np.arange(50) & 1).astype(np.uint8) >>> d = BurstDemod(dcode, spc=4, chip_rate=1e6, frame_syms=93) >>> sync = np.array([0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0], np.uint8) >>> d.set_sync(sync) # Barker-13: frame align + phase/sign fix
function burst_demod_symbols¶
The last demod()'s DEROTATED complex symbols — the constellation the LLRs are the real part of.
size_t burst_demod_symbols (
burst_demod_state_t * state,
size_t n,
float _Complex * out,
size_t max_out
)
Same span and same normalisation as burst_demod_llrs(): the whole frame, scaled to unit mean-|Re| by the burst's own estimate, so crealf(symbols[k]) is that bit's LLR up to est_n0.
The quadrature is why this exists. After derotation the real axis carries the signal and the imaginary axis carries noise alone, so Q is diagnostic: a residual phase error scales Re by cos(phi) WITHOUT adding noise, which makes it indistinguishable from a genuine amplitude or SNR loss in mean |LLR|, in LLR spread and in BER alike. Measured over 20000 BPSK symbols, a 30 degree phase error and an amplitude loss of cos(30 deg) agreed to three decimals in all three, and differed only in Q/I energy — 0.386 against 0.077 (doppler#1087). That is the difference between a pointing problem and a link-budget one, on a burst this object already characterised well enough to know.
Parameters:
stateDemodulator handle.nIgnored — the count is the last demod()'s frame.outReceives the symbols, one per frame bit.max_outCapacity ofout; see burst_demod_symbols_max_out().
Returns:
Symbols written — min(frame bits, max_out), or 0 if the last demod() produced no frame.
>>> import numpy as np
>>> from doppler.dsss import BurstDemod
>>> dcode = (np.arange(50) & 1).astype(np.uint8)
>>> d = BurstDemod(dcode, spc=4, chip_rate=1e6, frame_syms=93)
>>> d.set_sync(np.zeros(13, dtype=np.uint8))
>>> d.symbols_max_out(1) # one per frame symbol, as llrs()
93
function burst_demod_symbols_max_out¶
Max symbols burst_demod_symbols() writes: the frame's length.
Parameters:
stateDemodulator handle.nIgnored — the count is the last demod()'s frame.
The documentation for this class was generated from the following file native/inc/burst_demod/burst_demod_core.h