Skip to content

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_symbol is a non-integer double and 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:

  • code spreading code chips (0/1), length code_len.
  • code_len spreading code length in chips (> 0).
  • data data bits (0/1), length n_data; cycled if exhausted.
  • n_data data bit count (> 0).
  • chips_per_symbol chips per data symbol (> 0, typically non-integer).
  • n_chips chips to produce (the caller's requested span).
  • out output chip array (0/1) of n_chips elements.

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.

uint64_t dp_wfm_dsss_cont_edge (
    uint64_t k,
    double chips_per_symbol
) 

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:

  • k data-symbol index.
  • chips_per_symbol chips 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:

  • syms complex data symbols;
  • n_sym their count.
  • code PN chip code (0/1), length sf;
  • sf spreading factor.
  • out output chips, length n_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:

  • proto prototype FIR taps.
  • proto_len number of prototype taps.
  • num_phases interpolation factor (bank rows).
  • num_taps taps per phase; must satisfy num_phases * num_taps >= proto_len (use (proto_len + num_phases - 1) / num_phases).
  • bank output bank, row-major, length num_phases * num_taps.

function dp_wfm_rrc_polyphase_bank

Decompose the RRC pulse shape into a polyphase interpolation bank.

void dp_wfm_rrc_polyphase_bank (
    double beta,
    int sps,
    int span,
    float * 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:

  • beta roll-off in [0, 1].
  • sps samples per symbol (>= 1); also the number of phases.
  • span one-sided span in symbols (>= 1).
  • bank output bank, row-major, length sps * wfm_rrc_bank_ntaps(span).

function dp_wfm_rrc_taps

Fill taps with a unit-energy root-raised-cosine impulse response.

void dp_wfm_rrc_taps (
    double beta,
    int sps,
    int span,
    float * taps
) 

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:

  • beta roll-off in [0, 1].
  • sps samples per symbol (>= 1).
  • span one-sided span in symbols (>= 1).
  • taps output array of length wfm_rrc_ntaps(sps, span).

Public Static Functions Documentation

function wfm_cont_dsss_nchips

Chip count for dp_wfm_cont_dsss_chips : exactlyn_chips .

static inline size_t wfm_cont_dsss_nchips (
    size_t n_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.

static inline double wfm_rc_h (
    double t,
    double beta
) 

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:

  • t time in SYMBOL periods (T = 1), relative to the pulse centre.
  • beta roll-off in [0, 1].

Returns:

g(t), with g(0) = 1.

printf ("%.4f %.6f\n", wfm_rc_h (0.0, 0.35), wfm_rc_h (1.0, 0.35));
// 1.0000 0.000000

function wfm_rrc_bank_ntaps

Number of taps per phase in a dp_wfm_rrc_polyphase_bank :2*span + 1 .

static inline size_t wfm_rrc_bank_ntaps (
    int span
) 

Parameters:

  • span one-sided filter span in symbols (>= 1).

function wfm_rrc_h

Analytic root-raised-cosine impulse response at one instant.

static inline double wfm_rrc_h (
    double t,
    double beta
) 

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:

  • t time in SYMBOL periods (T = 1), relative to the pulse centre.
  • beta roll-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 .

static inline size_t wfm_rrc_ntaps (
    int sps,
    int span
) 

Parameters:

  • sps samples per symbol (>= 1).
  • span one-sided filter span in symbols (>= 1).

Macro Definition Documentation

define M_PI

#define M_PI `3.14159265358979323846`


The documentation for this class was generated from the following file native/inc/doppler/wfm/wfm_dsp.h