Skip to content

File ratesync_core.h

FileList > inc > ratesync > ratesync_core.h

Go to the source code of this file

RateSync — symbol-timing recovery on a matched-filter rate cascade. More...

  • #include "RateConverter/RateConverter_core.h"
  • #include "cic/cic_core.h"
  • #include "clib_common.h"
  • #include "dp_state.h"
  • #include "fir/fir_core.h"
  • #include "hbdecim/hbdecim_core.h"
  • #include "jm_perf.h"
  • #include "lockdet/lockdet_core.h"
  • #include "loop_filter/loop_filter_core.h"
  • #include "resamp/resamp_core.h"
  • #include "resample/resample_core.h"
  • #include "symsync/symsync_core.h"
  • #include "dp_tlm/dp_tlm_core.h"
  • #include "telemetry/telemetry_core.h"
  • #include "ber/ber_core.h"
  • #include "pn/pn_core.h"

Classes

Type Name
struct ratesync_loop_t
The symbol-timing loop, independent of what feeds it.
struct ratesync_state_t
RateSync state: a matched-filter cascade and the timing loop.
struct ratesync_tlm_t
Telemetry attachment: a borrowed context + this object's probe ids. NULL ctx (the default) means detached — every probe site is then one predicted-not-taken branch per recovered symbol.

Public Types

Type Name
enum ratesync__core_8h_1a61dadd085c1777f559549e05962b2c9e
Timing-error-detector selection for ratesync_state_t::ted.
enum ratesync__core_8h_1a726ca809ffd3d67ab4b8476646f26635
Matched-filter pulse shape.

Public Functions

Type Name
void ratesync_configure (ratesync_state_t * state, double bn, double zeta)
Recompute the loop gains for a new bandwidth/damping, keeping the timing estimate.
void ratesync_configure_lock_raw (ratesync_state_t * state, size_t avgs, double up_thresh, double down_thresh, uint32_t n_up, uint32_t n_down)
Set the lock detector's geometry directly.
ratesync_state_t * ratesync_create (double sps, int pulse, double beta, size_t span, size_t m, size_t num_phases, double bn, double zeta, int ted)
Create a RateSync instance.
void ratesync_destroy (ratesync_state_t * state)
Destroy a RateSync instance and release all memory.
double ratesync_get_bn (const ratesync_state_t * state)
int ratesync_get_clipped (const ratesync_state_t * state)
Has the cascade's CIC stage clipped its input since the last reset? Forwarded from the RateConverter: a CIC bounds its input to +-1.0 and clips silently past that, which no timing metric reveals. Always 0 when the plan has no CIC stage.
double ratesync_get_ctrl (const ratesync_state_t * state)
Current per-input control deviation steering the strobe.
double ratesync_get_lock_stat (const ratesync_state_t * state)
Last block-averaged lock statistic (the eye-opening ratio).
int ratesync_get_locked (const ratesync_state_t * state)
Current lock decision (1 = locked), verify-counted.
double ratesync_get_rate (const ratesync_state_t * state)
Smoothed tracked samples per symbol. Departs from the nominal sps by exactly the sample-clock offset being tracked, so it is the estimator a rate-disciplining caller reads.
void ratesync_get_state (const ratesync_state_t * state, void * blob)
Serialize the mutable state into blob .
double ratesync_get_timing_error (const ratesync_state_t * state)
Last normalised TED error — the loop stress.
void ratesync_loop_bind_cascade (ratesync_loop_t * l, const RateConverter_state_t * rc)
Read that geometry straight off a cascade.
void ratesync_loop_configure (ratesync_loop_t * l, double bn, double zeta)
Retune the loop; preserves the integrator (and so the lock).
void ratesync_loop_configure_lock_raw (ratesync_loop_t * l, size_t avgs, double up_thresh, double down_thresh, uint32_t n_up, uint32_t n_down)
Set the lock detector's geometry; see ratesync_configure_lock_raw() , which forwards here.
void ratesync_loop_get_state (const ratesync_loop_t * l, void * blob)
Serialize the loop's mutable state into blob .
void ratesync_loop_init (ratesync_loop_t * l, double sps, size_t m, double bn, double zeta, int ted)
Initialise a standalone timing loop.
void ratesync_loop_reset (ratesync_loop_t * l)
Re-seed the loop: integrator, strobe ring, lock detector and the prime countdown. Configuration and cascade geometry are kept.
void ratesync_loop_set_cascade (ratesync_loop_t * l, double term_rate, size_t prime_taps)
Tell the loop the geometry of the accumulator it steers.
int ratesync_loop_set_state (ratesync_loop_t * l, const void * blob)
Restore the loop's mutable state from blob .
int ratesync_loop_set_telemetry (ratesync_loop_t * l, dp_tlm_t * tlm, const char * prefix, uint32_t decim)
Register the six timing probes; see ratesync_set_telemetry() , which forwards here. NULLtlm detaches.
size_t ratesync_loop_state_bytes (const ratesync_loop_t * l)
Bytes ratesync_loop_get_state() writes (envelope + payload + the loop filter's child blob).
JM_FORCEINLINE JM_HOT int ratesync_loop_take_output (ratesync_loop_t * s, float _Complex y, float _Complex * y_out, int ted)
Fold one terminal-stage output into the timing loop.
void ratesync_loop_tlm_flush (const ratesync_loop_t * l)
Emit the timing loop's telemetry for the symbol just recovered.
void ratesync_reset (ratesync_state_t * state)
Reset to the post-create state: the cascade, the loop integrator, the lock detector, the strobe ring and the prime countdown.
void ratesync_set_bn (ratesync_state_t * state, double val)
int ratesync_set_state (ratesync_state_t * state, const void * blob)
Restore mutable state from blob into an identically built instance.
int ratesync_set_telemetry (ratesync_state_t * state, dp_tlm_t * tlm, const char * prefix, uint32_t decim)
Attach (or detach) a telemetry context and register the probes.
size_t ratesync_state_bytes (const ratesync_state_t * state)
Bytes ratesync_get_state() writes (envelope + payload + child).
JM_FORCEINLINE JM_HOT int ratesync_step (ratesync_state_t * s, float _Complex x, float _Complex * y_out)
Per-input timing step (the inline composition API).
JM_FORCEINLINE JM_HOT int ratesync_step_ted (ratesync_state_t * s, float _Complex x, float _Complex * y_out, int ted)
Per-input timing step with the TED selection as a parameter.
size_t ratesync_steps (ratesync_state_t * state, const float _Complex * x, size_t x_len, float _Complex * out, size_t max_out)
Recover symbols from a block of oversampled cf32 baseband.
size_t ratesync_steps_max_out (ratesync_state_t * state)
_Output-buffer hint for the generated binding; 0 means "the input
length is already a safe bound" — with_ sps >= m >= 2 a block can never yield more symbols than it has samples (mirrors symsync).

Macros

Type Name
define RATESYNC_LOCK_EPS 1e-12
define RATESYNC_LOOP_STATE_MAGIC [**DP\_FOURCC**](dp__state_8h.md#define-dp_fourcc) ('R', 'S', 'L', 'P')
define RATESYNC_LOOP_STATE_VERSION /* multi line expression */
define RATESYNC_MAX_M 8
define RATESYNC_STATE_MAGIC [**DP\_FOURCC**](dp__state_8h.md#define-dp_fourcc) ('R', 'A', 'T', 'S')
define RATESYNC_STATE_VERSION 2u /\* v2: running state moved into the loop \*/

Detailed Description

RateSync owns a RateConverter whose terminal stage carries the pulse (RateConverter_create_matched) and closes a timing loop around that stage's control port. It builds no filters of its own: the matched filter IS the cascade's last dot product, and the polyphase arm that dot product selects IS the fractional timing delay. One filter, no Farrow, no separate matched-filter pass.

Where SymbolSync separates the jobs (a matched FIR, then a Farrow interpolator steered by a timing NCO), this fuses them — and because the cascade in front is a full RateConverter, the fusion inherits its planning: HB/CIC stages do the bulk decimation for free, so the matched filter is sized by the POST-decimation rate. A matched filter at 256 input samples per symbol costs the same bank as one at 4.

Arbitrary rate, by construction. sps is a double — 4, 17.33389, an irrational ratio, or a slowly drifting clock — because the terminal stage's accumulator is a double and the loop only has to steer the strobe. That is the real-world case whenever the ADC clock is free-running against the symbol clock.

Two things this object gets right that are easy to get wrong

1. Nothing in the error path can vanish. The TED error is the raw detector output times ted_scale — a CONSTRUCT-TIME reciprocal of the detector's own slope against this pulse (symsync_ted_slope()), not a running power estimate. A running normaliser is what the error path used to carry, and getting it wrong killed the loop outright: normalising by the on-time energy |on|^2 alone divides by zero exactly when the strobe sits on the symbol transitions, which is precisely the state the loop must recover FROM. Measured, the error reached -91, the control drove the terminal stage's effective rate NEGATIVE, its accumulator stopped advancing, and the cascade emitted nothing ever again (2 symbols where 4000 were expected — a permanent death, not a transient). The fix at the time was to normalise by |on|^2 + |mid|^2: the same signal half a symbol apart, so the sum is bounded away from zero at every timing phase. That sum survives as the LOCK STATISTIC's normaliser, which is the only place it is still computed; the error path moved to the construct-time constant (RATESYNC_LOOP_STATE_VERSION 2 records the change), which settles the question rather than answering it — a constant cannot vanish.

Either way the conclusion holds and is what a caller cares about: RateSync needs no clamp on the control anywhere, and there is none in the source. Measured, ctrl stays inside a few hundredths driven from the worst initial offset at the widest recommended bn, bounded by the detector's own S-curve, which is bounded by construction (report §2.7).

2. The loop stays open until the cascade is primed. A cascade's first outputs are its delay lines filling, not signal (the eye statistic swings over its whole +-2 range through them). Steering on them is meaningless and was worth one lost acquisition in sixteen. ratesync_create() computes the prime length from the terminal bank's own geometry.

The T/2 role ambiguity resolves itself

Gardner needs an on-time strobe and a transition gate half a symbol earlier. Running at rate = m/sps and taking every m-th output as on-time makes that a parity count, which looks like it should be ambiguous — and a half-symbol error is indeed an equilibrium of the detector. It is an unstable one: measured over a fine sweep, each parity's S-curve has exactly two zeros per symbol, one at the eye centre with negative slope (stable) and one at the T/2 point with positive slope (unstable). The loop runs away from the wrong one on its own, so the parity does not matter and no eye-sign detector or counter flip is needed. (An earlier prototype ran two displaced banks to pin the roles structurally; measurement showed that buys nothing and costs double the multiplies.)

Measured

RRC-BPSK, noiseless, eight initial timing offsets each: every offset acquires, on every cascade the planner builds — HB + Resampler(1,rrc) at sps = 4, CIC(8) + Resampler(0.923,rrc) at 17.333, CIC(32) + Resampler(1,rrc) at 64.

The EVM those runs reach is deliberately not quoted here. A table of literals in a header is the documentation form of a snapshot nothing re-runs, and this one had drifted 3 to 4 dB optimistic by the time anything re-measured it. The live figures are regenerated on every push into src/doppler/track/tests/validation/ratesync/results.md — §2.4 for acquisition against the settling budget, §2.5 for the bn sweep — and that report's Limits section is executed by test_validation_limits.py.

What belongs here is the shape, which is stable. bn means the same thing on every planned cascade, and that is the point of referencing the control to the TERMINAL stage's rate rather than the cascade rate: the alternative is measured costing 18 dB. How closely depends on how narrow the loop is — the spread across the three cascades widens monotonically as bn narrows, from a few tenths of a dB at bn = 0.02 to several dB at 0.002, so read it as "within ~1 dB at the recommended settings" rather than as a universal. What that measurement does NOT establish is the mechanism: the cascades differ in front-end group delay and in residual ISI, and which of those a narrow loop stops averaging over is not determined. bn = 0.005 is the best of the recommended settings and bn = 0.01 the safe default; lower settings acquire too slowly to have settled inside a fixed record, so a record length that does not scale with 1/bn measures nothing there.

Lifecycle: create -> (step / steps / reset)* -> destroy

ratesync_state_t *rx = ratesync_create (17.33389, RATESYNC_PULSE_RRC, 0.35,
                                        8, 2, 1024, 0.01, 0.707,
                                        RATESYNC_TED_GARDNER);
float _Complex sym;
if (ratesync_step (rx, x, &sym))
  consume (sym);
ratesync_destroy (rx);

Public Types Documentation

enum ratesync__core_8h_1a61dadd085c1777f559549e05962b2c9e

Timing-error-detector selection for ratesync_state_t::ted.

enum ratesync__core_8h_1a61dadd085c1777f559549e05962b2c9e {
    RATESYNC_TED_GARDNER = 0,
    RATESYNC_TED_DTTL = 1
};


enum ratesync__core_8h_1a726ca809ffd3d67ab4b8476646f26635

Matched-filter pulse shape.

enum ratesync__core_8h_1a726ca809ffd3d67ab4b8476646f26635 {
    RATESYNC_PULSE_IANDD = RC_PULSE_IANDD,
    RATESYNC_PULSE_RRC = RC_PULSE_RRC
};

Aliases of the cascade's own vocabulary (rc_pulse_t) so one set of names covers the family; the pulse is built by the RateConverter, which is the only party that knows its own CIC geometry.


Public Functions Documentation

function ratesync_configure

Recompute the loop gains for a new bandwidth/damping, keeping the timing estimate.

void ratesync_configure (
    ratesync_state_t * state,
    double bn,
    double zeta
) 

Only the PI coefficients change; the integrator, and therefore the tracked rate and the lock, carries through untouched. Use it to narrow the loop after acquisition (a wide bn pulls in fast, a narrow one tracks with less jitter) without forcing a re-acquire.

Parameters:

  • state Must be non-NULL.
  • bn Loop noise bandwidth, normalised to the symbol rate.
  • zeta Damping factor (0.707 = critically damped).
    >>> import numpy as np
    >>> from doppler.track import RateSync
    >>> syms = np.where(np.random.default_rng(3).integers(0, 2, 3000) > 0,
    ...                 1.0, -1.0)
    >>> x = (0.25 * np.repeat(syms, 8)).astype(np.complex64)  # 8 samp/sym
    >>> rs = RateSync(sps=8.0, pulse="iandd", m=4, bn=0.01)
    >>> _ = rs.steps(x)              # acquire and lock
    >>> rs.locked
    True
    >>> rs.configure(0.002, 0.707)   # narrow the loop; lock is kept
    >>> round(rs.bn, 3)
    0.002
    >>> rs.locked
    True
    

function ratesync_configure_lock_raw

Set the lock detector's geometry directly.

void ratesync_configure_lock_raw (
    ratesync_state_t * state,
    size_t avgs,
    double up_thresh,
    double down_thresh,
    uint32_t n_up,
    uint32_t n_down
) 

The block size (avgs), a split declare/drop threshold pair on lock_stat (level hysteresis) and both verify counts (time hysteresis). Re-tuning clears the in-flight block sum and drops the lock, so the next decision uses only looks gathered under the new config.

Parameters:

  • state Must be non-NULL.
  • avgs Looks per decision; clamped >= 1.
  • up_thresh Declare threshold on lock_stat.
  • down_thresh Drop threshold; <= up_thresh for level hysteresis.
  • n_up Consecutive above-threshold decisions to declare.
  • n_down Consecutive below-threshold decisions to drop.
    >>> import numpy as np
    >>> from doppler.track import RateSync
    >>> syms = np.where(np.random.default_rng(3).integers(0, 2, 3000) > 0,
    ...                 1.0, -1.0)
    >>> x = (0.25 * np.repeat(syms, 8)).astype(np.complex64)
    >>> rs = RateSync(sps=8.0, pulse="iandd", m=4, bn=0.01)
    >>> _ = rs.steps(x)
    >>> rs.locked
    True
    >>> rs.configure_lock_raw(64, 0.5, 0.4, 2, 4)  # drops the lock
    >>> rs.locked
    False
    >>> rs.lock_stat                 # the in-flight block was cleared
    0.0
    

function ratesync_create

Create a RateSync instance.

ratesync_state_t * ratesync_create (
    double sps,
    int pulse,
    double beta,
    size_t span,
    size_t m,
    size_t num_phases,
    double bn,
    double zeta,
    int ted
) 

Builds a RateConverter(rate = m/sps, pulse, ..., pulse_sps = m) with CIC droop compensation on — folded into the bank, so it costs six taps per arm and no extra stage, and is worth ~28 dB of EVM on any cascade that plans a CIC. See RateConverter_create_matched().

** **

Present unit-amplitude symbols. This object carries no AGC, and that is deliberate: a receiver composing it already levels in its own front-end cascade (RateConverter_enable_agc(), one per receiver), so an AGC here would be a second one integrating against the first. The level to hit is not a tuned number — the TED normalises by its own construct-time slope, and that slope is computed for the reference the bank already defines, 10*log10(bank_e0 / bank_sps), which is ~0 dB because the bank normalises by its own pulse energy. See RateConverter_agc_ref_db(), which is defined for any matched cascade whether or not an AGC is enabled.

The level axis is two-sided and it is not monotone, so "as long as it is not clipping" is not a level check. Both ends cost EVM for one reason: the Gardner error carries an A^2 factor, so the input level multiplies the loop gain and the level axis IS the bn axis — too hot tracks noisily, too cold has not settled. Measured, EVM is flat to within a few tenths of a dB either side of the contracted level and falls off sharply outside that, by well over 15 dB at twice amplitude and again at a quarter of it (report §2.6, regenerated every push).

Nothing here reports either end reliably. Over-drive is reported only on the subset of plans that happen to contain a CIC: ratesync_get_clipped() is a CIC quantiser flag, and whether the plan HAS a CIC is the planner's decision, not the caller's — a CIC-free cascade (which is what sps = 8 plans) reads 0 however hard it is driven. Under-drive has no flag on any plan at all; that gap is tracked as gh-661. And locked is not a substitute for either: it answers "is the eye open", which even a badly mis-levelled loop eventually manages, so it declares lock while demodulating far worse. Judge the input level by measuring it, not by reading a flag off this object.

Parameters:

  • sps Nominal samples per symbol; any double >= m (17.33389 is as valid as 4). The bound is m, not 2, because the terminal stage must not be asked to interpolate: rate = m/sps <= 1.
  • pulse RATESYNC_PULSE_IANDD (rectangular/NRZ) or _RRC.
  • beta RRC roll-off in [0, 1] (ignored for the rectangle).
  • span One-sided RRC span in symbols (ignored for the rectangle, whose support is always one symbol).
  • m Terminal outputs per symbol: even, 2 <= m <= RATESYNC_MAX_M. Gardner needs the half-symbol gate, so m must be even and at least 2. The oversampled stream is a by-product, not an extra cost. Use m >= 4 with RATESYNC_PULSE_IANDD: the rectangle is one symbol wide, so at m = 2 its matched filter is a two-tap sum and the eye barely opens. Measured on an NRZ stream, m = 2 does not clear the lock detector's own declare threshold while m = 4 clears it comfortably, and the EVM between them differs by tens of dB. The rule rests on that SEPARATION, not on any particular pair of lock_stat values — those move with sps and with the stream, and report §2.7 sweeps them. The RRC spans many symbols and is unaffected.
  • num_phases Matched-filter arms; power of two (1024 is a good default). Sets the fractional-timing resolution to 1/num_phases of an output period.
  • bn Loop noise bandwidth, normalised to the symbol rate.
  • zeta Damping factor (0.707 = critically damped).
  • ted RATESYNC_TED_GARDNER (blind) or RATESYNC_TED_DTTL (decision-directed; BPSK/QPSK only).

Returns:

Heap-allocated state, or NULL if a parameter is out of range or allocation fails.

Note:

Caller must call ratesync_destroy() when done.


function ratesync_destroy

Destroy a RateSync instance and release all memory.

void ratesync_destroy (
    ratesync_state_t * state
) 

Parameters:

  • state May be NULL.

function ratesync_get_bn

double ratesync_get_bn (
    const ratesync_state_t * state
) 

function ratesync_get_clipped

Has the cascade's CIC stage clipped its input since the last reset? Forwarded from the RateConverter: a CIC bounds its input to +-1.0 and clips silently past that, which no timing metric reveals. Always 0 when the plan has no CIC stage.

int ratesync_get_clipped (
    const ratesync_state_t * state
) 


function ratesync_get_ctrl

Current per-input control deviation steering the strobe.

double ratesync_get_ctrl (
    const ratesync_state_t * state
) 


function ratesync_get_lock_stat

Last block-averaged lock statistic (the eye-opening ratio).

double ratesync_get_lock_stat (
    const ratesync_state_t * state
) 

This, not an error-vector magnitude, is the honest lock indicator: a single cycle slip during acquisition drags a windowed EVM by 20 dB while the eye stays wide open at +0.75. Judge lock here.


function ratesync_get_locked

Current lock decision (1 = locked), verify-counted.

int ratesync_get_locked (
    const ratesync_state_t * state
) 


function ratesync_get_rate

Smoothed tracked samples per symbol. Departs from the nominal sps by exactly the sample-clock offset being tracked, so it is the estimator a rate-disciplining caller reads.

double ratesync_get_rate (
    const ratesync_state_t * state
) 


function ratesync_get_state

Serialize the mutable state into blob .

void ratesync_get_state (
    const ratesync_state_t * state,
    void * blob
) 


function ratesync_get_timing_error

Last normalised TED error — the loop stress.

double ratesync_get_timing_error (
    const ratesync_state_t * state
) 


function ratesync_loop_bind_cascade

Read that geometry straight off a cascade.

void ratesync_loop_bind_cascade (
    ratesync_loop_t * l,
    const RateConverter_state_t * rc
) 

Walks rc to its terminal stage and forwards its rate and tap count to ratesync_loop_set_cascade(). Every owner of this loop owns a RateConverter somewhere — RateSync directly, the receivers inside their DDC — so the walk lives here once rather than in each of them.

Parameters:

  • l Must be non-NULL.
  • rc The cascade whose terminal stage the loop steers.

function ratesync_loop_configure

Retune the loop; preserves the integrator (and so the lock).

void ratesync_loop_configure (
    ratesync_loop_t * l,
    double bn,
    double zeta
) 


function ratesync_loop_configure_lock_raw

Set the lock detector's geometry; see ratesync_configure_lock_raw() , which forwards here.

void ratesync_loop_configure_lock_raw (
    ratesync_loop_t * l,
    size_t avgs,
    double up_thresh,
    double down_thresh,
    uint32_t n_up,
    uint32_t n_down
) 


function ratesync_loop_get_state

Serialize the loop's mutable state into blob .

void ratesync_loop_get_state (
    const ratesync_loop_t * l,
    void * blob
) 


function ratesync_loop_init

Initialise a standalone timing loop.

void ratesync_loop_init (
    ratesync_loop_t * l,
    double sps,
    size_t m,
    double bn,
    double zeta,
    int ted
) 

Sets the loop filter (update period = one symbol, so bn is normalised to the symbol rate) and the default lock-detector geometry, then seeds every running field. The caller must still describe the accumulator being steered with ratesync_loop_set_cascade() before pushing outputs through.

Parameters:

  • l Loop to initialise. Must be non-NULL.
  • sps Nominal samples per symbol (any double).
  • m Terminal outputs per symbol; even, 2..RATESYNC_MAX_M.
  • bn Loop noise bandwidth, normalised to the symbol rate.
  • zeta Damping factor.
  • ted RATESYNC_TED_GARDNER or RATESYNC_TED_DTTL.

function ratesync_loop_reset

Re-seed the loop: integrator, strobe ring, lock detector and the prime countdown. Configuration and cascade geometry are kept.

void ratesync_loop_reset (
    ratesync_loop_t * l
) 

Parameters:

  • l Must be non-NULL.

function ratesync_loop_set_cascade

Tell the loop the geometry of the accumulator it steers.

void ratesync_loop_set_cascade (
    ratesync_loop_t * l,
    double term_rate,
    size_t prime_taps
) 

Parameters:

  • l Loop. Must be non-NULL.
  • term_rate The terminal stage's own rate. ctrl is referenced to this, not to the overall cascade rate — they differ by the whole integer decimation in front, which would under-drive the loop by exactly that factor.
  • prime_taps The terminal bank's tap count; the loop discards prime_taps + 1 outputs before closing, because those are the delay lines filling rather than signal.

function ratesync_loop_set_state

Restore the loop's mutable state from blob .

int ratesync_loop_set_state (
    ratesync_loop_t * l,
    const void * blob
) 

Returns:

DP_OK, or DP_ERR_INVALID if any envelope rejects.


function ratesync_loop_set_telemetry

Register the six timing probes; see ratesync_set_telemetry() , which forwards here. NULLtlm detaches.

int ratesync_loop_set_telemetry (
    ratesync_loop_t * l,
    dp_tlm_t * tlm,
    const char * prefix,
    uint32_t decim
) 


function ratesync_loop_state_bytes

Bytes ratesync_loop_get_state() writes (envelope + payload + the loop filter's child blob).

size_t ratesync_loop_state_bytes (
    const ratesync_loop_t * l
) 


function ratesync_loop_take_output

Fold one terminal-stage output into the timing loop.

JM_FORCEINLINE  JM_HOT int ratesync_loop_take_output (
    ratesync_loop_t * s,
    float _Complex y,
    float _Complex * y_out,
    int ted
) 

The whole of the loop's per-output work, and the reason the loop is a struct of its own: it never touches the cascade, so a receiver that owns its cascade inside a DDC drives this with exactly the same call RateSync makes.

Parameters:

  • s Loop state. Must be non-NULL.
  • y One terminal-stage output.
  • y_out Receives the symbol when the return is 1.
  • ted RATESYNC_TED_GARDNER or RATESYNC_TED_DTTL — pass a literal for a specialised (branch-free) instantiation.

Returns:

1 if this output was an on-time strobe that produced a symbol.


function ratesync_loop_tlm_flush

Emit the timing loop's telemetry for the symbol just recovered.

void ratesync_loop_tlm_flush (
    const ratesync_loop_t * l
) 

Out-of-line on purpose: the emit machinery must not inline into the per-sample hot loop (the same body-growth cost symsync measured). Callers gate on l->tlm.ctx, so the detached cost is one predicted-not-taken branch per symbol.

Parameters:

  • l Loop with a non-NULL tlm.ctx (caller-checked).

function ratesync_reset

Reset to the post-create state: the cascade, the loop integrator, the lock detector, the strobe ring and the prime countdown.

void ratesync_reset (
    ratesync_state_t * state
) 

Configuration (sps, pulse, bank, bn, zeta, ted, lock geometry) is kept; only the running state is cleared, so a re-run of the same stream from a reset object reproduces its first-run symbols bit for bit.

Parameters:

  • state Must be non-NULL.
    >>> import numpy as np
    >>> from doppler.track import RateSync
    >>> syms = np.where(np.random.default_rng(3).integers(0, 2, 3000) > 0,
    ...                 1.0, -1.0)
    >>> x = (0.25 * np.repeat(syms, 8)).astype(np.complex64)
    >>> rs = RateSync(sps=8.0, pulse="iandd", m=4, bn=0.01)
    >>> first = np.array(rs.steps(x))
    >>> rs.reset()
    >>> rs.ctrl, rs.locked           # back to the post-create state
    (0.0, False)
    >>> bool(np.array_equal(first, np.array(rs.steps(x))))  # reproducible
    True
    

function ratesync_set_bn

void ratesync_set_bn (
    ratesync_state_t * state,
    double val
) 

function ratesync_set_state

Restore mutable state from blob into an identically built instance.

int ratesync_set_state (
    ratesync_state_t * state,
    const void * blob
) 

Returns:

DP_OK, or DP_ERR_INVALID if any envelope rejects.


function ratesync_set_telemetry

Attach (or detach) a telemetry context and register the probes.

int ratesync_set_telemetry (
    ratesync_state_t * state,
    dp_tlm_t * tlm,
    const char * prefix,
    uint32_t decim
) 

Registers six probes, emitted once per recovered symbol and further thinned by decim: "<prefix>.e" (normalised TED error), "<prefix>.ctrl" (the per-input control steering the strobe), "<prefix>.rate" (tracked samples/symbol), "<prefix>.lock" (last block-averaged lock_signal), "<prefix>.locked" (0/1) and "<prefix>.mu" (the timing NCO's fractional phase — see resamp_get_ctrl_acc()). Passing NULL detaches. Setup path, never hot: the context is borrowed and must outlive the attachment (SPSC rules in dp_tlm/dp_tlm_core.h).

The three form one readable picture of the loop: e is what the detector saw, ctrl is what the filter did about it, and mu is where the sampling instant ended up as a result — the only one of the three that is a physical position rather than a correction.

Parameters:

  • state Must be non-NULL.
  • tlm Telemetry context to attach, or NULL to detach.
  • prefix Probe-name prefix, e.g. "sync".
  • decim Emit every decim-th symbol; >= 1.

Returns:

DP_OK, or DP_ERR_INVALID when the probe table cannot take all six probes (the attach fails whole; the object stays detached).

>>> from doppler.track import RateSync
>>> from doppler.telemetry import Telemetry
>>> tlm = Telemetry(1 << 14)
>>> rs = RateSync(sps=8.0, pulse="iandd", m=4, bn=0.01)
>>> rs.set_telemetry(tlm, "sync")   # register the six timing probes
>>> tlm.probe_count
6
>>> "sync.rate" in tlm.probe_names   # tracked samples/symbol
True


function ratesync_state_bytes

Bytes ratesync_get_state() writes (envelope + payload + child).

size_t ratesync_state_bytes (
    const ratesync_state_t * state
) 


function ratesync_step

Per-input timing step (the inline composition API).

JM_FORCEINLINE  JM_HOT int ratesync_step (
    ratesync_state_t * s,
    float _Complex x,
    float _Complex * y_out
) 

The public form of ratesync_step_ted(): dispatches on the configured detector and flushes telemetry when attached.

Parameters:

  • s State. Must be non-NULL.
  • x One input sample.
  • y_out Receives the symbol when the return is 1.

Returns:

1 if a symbol was emitted (into y_out), 0 otherwise.


function ratesync_step_ted

Per-input timing step with the TED selection as a parameter.

JM_FORCEINLINE  JM_HOT int ratesync_step_ted (
    ratesync_state_t * s,
    float _Complex x,
    float _Complex * y_out,
    int ted
) 

The workhorse behind ratesync_step()/ratesync_steps(). Pushes one input through the cascade at the current control deviation, which emits up to two terminal-stage outputs for that one input: rate = m/sps <= 1 bounds it at two, and a terminal rate at or near 1.0 — what an integer sps plans — reaches that bound whenever the control has pushed the accumulator over. Taking only the first would permanently shift the strobe parity, which is why the output buffer holds several and this function drains all of them.

Every m-th output is an on-time strobe and the output m/2 back is the transition gate. On a strobe the TED compares the two, the PI loop steers the next control, and the on-time sample is the recovered symbol. With m >= 2 at most one strobe can fall among a single input's outputs, so returning one symbol per input is still correct.

Passing a literal ted lets the force-inlined body constant-fold the detector branch away, exactly as symsync_step_ted() does.

Parameters:

  • s State. Must be non-NULL.
  • x One input sample.
  • y_out Receives the symbol when the return is 1.
  • ted RATESYNC_TED_GARDNER or RATESYNC_TED_DTTL — pass a literal for a specialised (branch-free) instantiation.

Returns:

1 if a symbol was emitted (into y_out), 0 otherwise.


function ratesync_steps

Recover symbols from a block of oversampled cf32 baseband.

size_t ratesync_steps (
    ratesync_state_t * state,
    const float _Complex * x,
    size_t x_len,
    float _Complex * out,
    size_t max_out
) 

ratesync_step() in a loop, with the TED specialised per detector; state carries across calls, so contiguous blocks give the same symbols as one large block.

Parameters:

  • state Must be non-NULL.
  • x Input samples.
  • x_len Number of inputs.
  • out Recovered symbols.
  • max_out Capacity of out.

Returns:

Symbols written to out.

>>> import numpy as np
>>> from doppler.track import RateSync
>>> syms = np.where(np.random.default_rng(3).integers(0, 2, 3000) > 0,
...                 1.0, -1.0)
>>> x = (0.25 * np.repeat(syms, 8)).astype(np.complex64)  # 8 samp/sym
>>> rs = RateSync(sps=8.0, pulse="iandd", m=4, bn=0.01)
>>> y = rs.steps(x)             # one symbol per transmitted symbol
>>> round(rs.rate, 2)           # tracked samples per symbol
8.0
>>> bool(rs.lock_stat > 0.55)   # the timing loop has locked
True


function ratesync_steps_max_out

Output-buffer hint for the generated binding; 0 means "the input length is already a safe bound" — with sps >= m >= 2 a block can never yield more symbols than it has samples (mirrors symsync).

size_t ratesync_steps_max_out (
    ratesync_state_t * state
) 


Macro Definition Documentation

define RATESYNC_LOCK_EPS

#define RATESYNC_LOCK_EPS `1e-12`

define RATESYNC_LOOP_STATE_MAGIC

#define RATESYNC_LOOP_STATE_MAGIC `DP_FOURCC ('R', 'S', 'L', 'P')`

define RATESYNC_LOOP_STATE_VERSION

#define RATESYNC_LOOP_STATE_VERSION `/* multi line expression */`

define RATESYNC_MAX_M

#define RATESYNC_MAX_M `8`

Largest supported outputs-per-symbol (bounds the strobe ring in-struct).


define RATESYNC_STATE_MAGIC

#define RATESYNC_STATE_MAGIC `DP_FOURCC ('R', 'A', 'T', 'S')`

define RATESYNC_STATE_VERSION

#define RATESYNC_STATE_VERSION `2u /* v2: running state moved into the loop */`


The documentation for this class was generated from the following file native/inc/ratesync/ratesync_core.h