Skip to content

File wfm_dsp.h

FileList > inc > wfm > wfm_dsp.h

Go to the source code of this file

DSSS spreading + root-raised-cosine pulse shaping (Phase B). More...

  • #include "clib_common.h"
  • #include <math.h>

Public Functions

Type Name
size_t 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.
void 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.
size_t wfm_frame_dsss_chips (const uint8_t * acq_code, size_t acq_len, size_t acq_reps, const uint8_t * data_code, size_t data_len, const uint8_t * sync, size_t sync_len, const uint8_t * payload, size_t payload_len, int crc, uint8_t * out)
Build a two-code DSSS burst as one flat 0/1 chip pattern.
size_t wfm_frame_dsss_nchips (size_t acq_len, size_t acq_reps, size_t data_len, size_t sync_len, size_t payload_len, int crc)
Chip count of a DSSS burst frame (sizes wfm_frame_dsss_chips ).
void 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 wfm_rrc_polyphase_bank (double beta, int sps, int span, float * bank)
Decompose the RRC pulse shape into a polyphase interpolation bank.
void 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 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 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 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: * wfm_dsss_spread: multiply each data symbol by a PN chip code. * wfm_rrc_taps: a unit-energy root-raised-cosine FIR (matched-filter pulse shape), applied by upsample + FIR.

Public Functions Documentation

function wfm_cont_dsss_chips

Build a CONTINUOUS, ASYNCHRONOUS DSSS chip pattern.

size_t 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 wfm_frame_dsss_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. 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 wfm_dsss_spread

Spread n_sym complex data symbols by a binary PN code.

void 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 wfm_frame_dsss_chips

Build a two-code DSSS burst as one flat 0/1 chip pattern.

size_t wfm_frame_dsss_chips (
    const uint8_t * acq_code,
    size_t acq_len,
    size_t acq_reps,
    const uint8_t * data_code,
    size_t data_len,
    const uint8_t * sync,
    size_t sync_len,
    const uint8_t * payload,
    size_t payload_len,
    int crc,
    uint8_t * out
) 

The transmit side of burst_demod's frame contract, assembled in one place so TX and RX can never drift:

[ acq_code × acq_reps | (sync | payload | crc16(payload)) ⊕ data_code ]

The preamble is the unmodulated repeated acquisition code (no data on it — a pure coherent-integration target). Every frame bit is then spread by the (distinct) data code: chip j of frame bit b is b ^ data_code[j]. The CRC-16-CCITT trailer (dp_crc16.h) is computed over the payload bits only and spread MSB-first. Mapping chips to ±1 (BPSK) is the synth's job.

Parameters:

  • acq_code preamble code (0/1), length acq_len; NULL when acq_len*acq_reps == 0.
  • acq_len preamble code length in chips.
  • acq_reps preamble repetitions.
  • data_code payload spreading code (0/1), length data_len.
  • data_len chips per frame symbol (the spreading factor).
  • sync frame-sync word bits (0/1), length sync_len; NULL ok.
  • sync_len sync word length in bits.
  • payload payload bits (0/1), length payload_len; NULL ok.
  • payload_len payload length in bits.
  • crc non-zero: append the CRC-16 trailer after the payload.
  • out output chip array (0/1) of wfm_frame_dsss_nchips(...) elements.

Returns:

Chips written, or 0 on invalid geometry (see wfm_frame_dsss_nchips).


function wfm_frame_dsss_nchips

Chip count of a DSSS burst frame (sizes wfm_frame_dsss_chips ).

size_t wfm_frame_dsss_nchips (
    size_t acq_len,
    size_t acq_reps,
    size_t data_len,
    size_t sync_len,
    size_t payload_len,
    int crc
) 

acq_len*acq_reps + (sync_len + payload_len + crc_bits) * data_len, where crc_bits is 16 when crc is set and there are payload bits, else 0 (a CRC over nothing protects nothing). Returns 0 when the geometry is invalid: frame bits present but no data code, or nothing to transmit at all.

Parameters:

  • acq_len preamble code length in chips (0 = no preamble).
  • acq_reps preamble repetitions (0 = no preamble).
  • data_len payload spreading-code length (chips per symbol).
  • sync_len frame-sync word length in bits (0 = none).
  • payload_len payload length in bits.
  • crc non-zero: a CRC-16 trailer follows the payload.

Returns:

Total burst chips, or 0 if the geometry is invalid/empty.


function wfm_polyphase_bank

Deal an arbitrary FIR prototype into a polyphase interpolation bank.

void 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 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 wfm_rrc_polyphase_bank

Decompose the RRC pulse shape into a polyphase interpolation bank.

void 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 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 wfm_rrc_taps(beta, sps, span) scaled by sqrt(sps) — the same unit-average-power scaling 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 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 wfm_rrc_taps

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

void 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 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 (wfm_frame_dsss_nchips / wfm_frame_dsss_chips) 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 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 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. 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 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/wfm/wfm_dsp.h