File wfm_dsp.h¶
FileList > doppler > wfm > wfm_dsp.h
Go to the source code of this file
DSSS spreading + root-raised-cosine pulse shaping (Phase B). More...
#include "doppler/clib_common.h"#include "doppler/util/util_core.h"#include <math.h>
Public Functions¶
| Type | Name |
|---|---|
| size_t | dp_wfm_cont_dsss_chips (const uint8_t * code, size_t code_len, const uint8_t * data, size_t n_data, double chips_per_symbol, size_t n_chips, uint8_t * out) Build a CONTINUOUS, ASYNCHRONOUS DSSS chip pattern. |
| uint64_t | dp_wfm_dsss_cont_edge (uint64_t k, double chips_per_symbol) The first chip of data symbol k on a continuous DSSS stream: the ONE symbol clock. |
| void | dp_wfm_dsss_spread (const float _Complex * syms, size_t n_sym, const uint8_t * code, size_t sf, float _Complex * out) Spread n_sym complex data symbols by a binary PN code. |
| void | dp_wfm_polyphase_bank (const float * proto, size_t proto_len, size_t num_phases, size_t num_taps, float * bank) Deal an arbitrary FIR prototype into a polyphase interpolation bank. |
| void | dp_wfm_rrc_polyphase_bank (double beta, int sps, int span, float * bank) Decompose the RRC pulse shape into a polyphase interpolation bank. |
| void | dp_wfm_rrc_taps (double beta, int sps, int span, float * taps) Fill taps with a unit-energy root-raised-cosine impulse response. |
Public Static Functions¶
| Type | Name |
|---|---|
| size_t | wfm_cont_dsss_nchips (size_t n_chips) Chip count for dp_wfm_cont_dsss_chips : exactlyn_chips . |
| double | wfm_rc_h (double t, double beta) The MATCHED pair's composite pulse: rrc * rrc , in closed form. |
| size_t | wfm_rrc_bank_ntaps (int span) Number of taps per phase in a dp_wfm_rrc_polyphase_bank :2*span + 1 . |
| double | wfm_rrc_h (double t, double beta) Analytic root-raised-cosine impulse response at one instant. |
| size_t | wfm_rrc_ntaps (int sps, int span) Number of taps a dp_wfm_rrc_taps call produces:2*span*sps + 1 . |
Macros¶
| Type | Name |
|---|---|
| define | M_PI 3.14159265358979323846 |
Detailed Description¶
Two pure DSP primitives the engine/composer use to build spread-spectrum and band-limited waveforms: * dp_wfm_dsss_spread: multiply each data symbol by a PN chip code. * dp_wfm_rrc_taps: a unit-energy root-raised-cosine FIR (matched-filter pulse shape), applied by upsample + FIR.
Public Functions Documentation¶
function dp_wfm_cont_dsss_chips¶
Build a CONTINUOUS, ASYNCHRONOUS DSSS chip pattern.
size_t dp_wfm_cont_dsss_chips (
const uint8_t * code,
size_t code_len,
const uint8_t * data,
size_t n_data,
double chips_per_symbol,
size_t n_chips,
uint8_t * out
)
The continuous counterpart to dp_wfm_dsss_desc_chips. Two differences, both required by a continuously-transmitting spread carrier (CCSDS command-link style) rather than a bounded burst:
- Continuous: no preamble, no sync word, no CRC. The spreading code repeats end to end and data rides on it the whole way.
- Asynchronous: the data-symbol clock is independent of the code epoch, so
chips_per_symbolis a non-integerdoubleand symbol boundaries land inside code epochs. The burst builder spreads exactly one bit per full code period — synchronous by construction, integer always.
Chip i carries code[i % code_len] ^ data[floor(i / chips_per_symbol)], so both clocks advance independently off the same chip index; the symbol edges are dp_wfm_dsss_cont_edge's, the one symbol clock. Because the symbol index is a floor of a fractional quotient, consecutive symbols legitimately span different numbers of chips (1136 or 1137 at SPEC.md's 3.069 Mcps / 2700 bps) — that jitter IS the asynchronicity, not an artifact.
Materialising the pattern up front, exactly as the burst builder does, is what lets the synth's existing cyclic chip latch play it back unchanged: no new per-sample branch, no new running state, no serialization change.
Parameters:
codespreading code chips (0/1), lengthcode_len.code_lenspreading code length in chips (> 0).datadata bits (0/1), lengthn_data; cycled if exhausted.n_datadata bit count (> 0).chips_per_symbolchips per data symbol (> 0, typically non-integer).n_chipschips to produce (the caller's requested span).outoutput chip array (0/1) ofn_chipselements.
Returns:
Chips written (== n_chips), or 0 on invalid geometry.
function dp_wfm_dsss_cont_edge¶
The first chip of data symbol k on a continuous DSSS stream: the ONE symbol clock.
Chip n belongs to data symbol floor(n / chips_per_symbol), so symbol k opens at the smallest n whose quotient reaches k. Every place that needs the clock asks this function the synth's per-chip kernel (wfm_synth_cont_dsss_chip) for each symbol edge, the run length of a data source (dp_wfm_source_data_samples) for the edge after the last symbol, and dp_wfm_cont_dsss_chips so a waveform and its length cannot disagree about where a symbol starts.
Exact under -ffast-math, by construction. The quotient is an IEEE division, never a multiply by the reciprocal: this tree builds with -ffast-math, whose reciprocal rewrite of a division by an invariant moved a symbol edge by a chip (at 6138000 / 2 / 2700 chips a symbol, symbol 51 opens on chip 57970; the reciprocal said 57971). The function turns that rewrite off for itself and is out of line, so no caller's loop can reshape it (doppler#1725).
Parameters:
kdata-symbol index.chips_per_symbolchips per data symbol (>= 1; non-integer is the asynchronous case).
Returns:
the first chip of symbol k; 0 for symbol 0.
#include <doppler/wfm/wfm_dsp.h>
int
main (void)
{
const double cps = (6138000.0 / 2.0) / 2700.0; // 1136.67 chips/symbol
// symbol 1 opens on chip 1137 = ceil(1136.67); symbol 51 on 57970,
// where ceil(51 * cps) = 57971 is a chip late in floating point
return dp_wfm_dsss_cont_edge (0, cps) != 0
|| dp_wfm_dsss_cont_edge (1, cps) != 1137
|| dp_wfm_dsss_cont_edge (51, cps) != 57970;
}
function dp_wfm_dsss_spread¶
Spread n_sym complex data symbols by a binary PN code.
void dp_wfm_dsss_spread (
const float _Complex * syms,
size_t n_sym,
const uint8_t * code,
size_t sf,
float _Complex * out
)
out[i*sf + j] = syms[i] * (code[j] ? -1 : +1) — each symbol is repeated across sf chips, sign-flipped per code chip. Output length is n_sym*sf. Works for BPSK (real syms) and QPSK (complex syms).
Parameters:
symscomplex data symbols;n_symtheir count.codePN chip code (0/1), lengthsf;sfspreading factor.outoutput chips, lengthn_sym * sf.
function dp_wfm_polyphase_bank¶
Deal an arbitrary FIR prototype into a polyphase interpolation bank.
void dp_wfm_polyphase_bank (
const float * proto,
size_t proto_len,
size_t num_phases,
size_t num_taps,
float * bank
)
The pure decomposition shared by every polyphase-bank builder: phase p gets the prototype taps that land on output samples of residue p, so bank[p*num_taps + t] = proto[t*num_phases + p] (zero-padded past proto_len). Row-major, num_phases * num_taps floats — exactly the layout dp_resamp_create_custom(num_phases, num_taps, bank, rate) consumes. Interpolate an input stream by num_phases (rate = num_phases) with the resulting bank and you recompute the dense proto convolution from only the nonzero upsampled contributions.
Parameters:
protoprototype FIR taps.proto_lennumber of prototype taps.num_phasesinterpolation factor (bank rows).num_tapstaps per phase; must satisfynum_phases * num_taps >= proto_len(use(proto_len + num_phases - 1) / num_phases).bankoutput bank, row-major, lengthnum_phases * num_taps.
function dp_wfm_rrc_polyphase_bank¶
Decompose the RRC pulse shape into a polyphase interpolation bank.
The dense pulse shaper upsamples a symbol stream by sps (one impulse per sps samples, the rest hard zeros) then runs the full dp_wfm_rrc_taps FIR over it — (sps-1)/sps of every tap-multiply hits a structural zero. The polyphase form computes the identical convolution from only the nonzero contributions: it splits the length-wfm_rrc_ntaps(sps, span) prototype into sps phases of wfm_rrc_bank_ntaps(span) taps each, so phase p selects the subset of prototype taps that land on output samples of residue p.
The prototype is dp_wfm_rrc_taps(beta, sps, span) scaled by sqrt(sps) — the same unit-average-power scaling dp_wfm_synth_set_rrc applies to the dense taps, folded in here so the two paths shape at byte-comparable amplitude. The row-major layout bank[p*num_taps + t] = proto[t*sps + p] (zero-padded past the final partial tap) is exactly the decomposition resamp's own Kaiser bank uses, so the bank drops straight into dp_resamp_create_custom(sps,
wfm_rrc_bank_ntaps(span), bank, sps) as an interpolate-by-sps shaper.
Unlike resamp's Kaiser prototype (which carries a ×num_phases gain to compensate interpolation energy spreading), the RRC prototype carries no such gain: the interpolate path reproduces the dense FIR output to float precision with the raw scaled taps.
Parameters:
betaroll-off in[0, 1].spssamples per symbol (>= 1); also the number of phases.spanone-sided span in symbols (>= 1).bankoutput bank, row-major, lengthsps * wfm_rrc_bank_ntaps(span).
function dp_wfm_rrc_taps¶
Fill taps with a unit-energy root-raised-cosine impulse response.
Length is wfm_rrc_ntaps(sps, span); the response is symmetric about the centre tap and normalised so sum(taps^2) == 1 (so cascading TX·RX gives a Nyquist raised cosine). The t = 0 and t = ±1/(4β) singularities are handled by their closed-form limits.
Parameters:
betaroll-off in[0, 1].spssamples per symbol (>= 1).spanone-sided span in symbols (>= 1).tapsoutput array of lengthwfm_rrc_ntaps(sps, span).
Public Static Functions Documentation¶
function wfm_cont_dsss_nchips¶
Chip count for dp_wfm_cont_dsss_chips : exactlyn_chips .
Trivial, but present so the two continuous entry points mirror the burst pair (dp_wfm_dsss_desc_nchips / dp_wfm_dsss_desc_chips, in wfm/wfm_frame.h) and callers size their buffer through a named function rather than an open-coded expression.
function wfm_rc_h¶
The MATCHED pair's composite pulse: rrc * rrc , in closed form.
A root-raised cosine convolved with itself is a raised cosine, so the pulse a matched receiver actually sees needs no convolution and no table — which is what lets a constructor evaluate it. Normalised to g(0) = 1, the level a unity-gain matched cascade delivers (see dp_RateConverter_gain()), so g(t) IS the recovered symbol amplitude at timing offset t.
Nyquist by construction: g(k) = 0 at every non-zero integer k, which is why a timing error and not an amplitude error is what inter-symbol interference looks like here.
The removable singularity at t = ±1/(2β) is handled by its closed-form limit; t = 0 needs none (the sinc limit is taken explicitly).
Parameters:
ttime in SYMBOL periods (T = 1), relative to the pulse centre.betaroll-off in[0, 1].
Returns:
g(t), with g(0) = 1.
function wfm_rrc_bank_ntaps¶
Number of taps per phase in a dp_wfm_rrc_polyphase_bank :2*span + 1 .
Parameters:
spanone-sided filter span in symbols (>= 1).
function wfm_rrc_h¶
Analytic root-raised-cosine impulse response at one instant.
The RRC formula itself, evaluated at an arbitrary continuous time — the single source of truth every RRC consumer samples. dp_wfm_rrc_taps() walks this on the uniform 1/sps grid and normalises; a receiver's polyphase matched-filter bank (RateConverter's pulse-shaped terminal stage) samples it at num_phases * num_taps instants that are NOT a uniform sub-multiple of the input grid, which is why the point evaluator is public: an arbitrary (non-integer) samples-per-symbol bank cannot be built by decomposing an integer-oversampled prototype, and a second copy of this formula is exactly the kind of peer implementation that drifts.
Both removable singularities are handled by their closed-form limits: the 0/0 at t = 0, and the 0/0 at t = ±1/(4β) where the denominator's 1 - (4βt)^2 vanishes.
Parameters:
ttime in SYMBOL periods (T = 1), relative to the pulse centre.betaroll-off in[0, 1].
Returns:
h(t), unnormalised (peak ≈ 1 - β + 4β/π at t = 0).
function wfm_rrc_ntaps¶
Number of taps a dp_wfm_rrc_taps call produces:2*span*sps + 1 .
Parameters:
spssamples per symbol (>= 1).spanone-sided filter span in symbols (>= 1).
Macro Definition Documentation¶
define M_PI¶
The documentation for this class was generated from the following file native/inc/doppler/wfm/wfm_dsp.h