File ddcr_core.h¶
FileList > ddcr > ddcr_core.h
Go to the source code of this file
Real-input Digital Down-Converter — halfband R2C + LO + cascade. More...
#include <complex.h>#include <stdbool.h>#include <stddef.h>#include "lo/lo_core.h"#include "RateConverter/RateConverter_core.h"#include "resamp/resamp_core.h"#include "hbdecim/hbdecim_core.h"#include "hbdecim/hbdecim_r2c_core.h"#include "cic/cic_core.h"#include "fir/fir_core.h"#include "resample/resample_core.h"#include "agc/agc_core.h"#include "dp_tlm/dp_tlm_core.h"
Classes¶
| Type | Name |
|---|---|
| struct | ddcr_extra_t |
| struct | ddcr_state DdcR state — the real-to-complex front end, an LO and a cascade. |
Public Types¶
| Type | Name |
|---|---|
| typedef struct ddcr_state | ddcr_state_t DdcR state — the real-to-complex front end, an LO and a cascade. |
Public Functions¶
| Type | Name |
|---|---|
| ddcr_state_t * | ddcr_create (double norm_freq, double rate) Create a real-input Digital Down-Converter (Architecture D2). The signal chain is: halfband R2C (2:1, bakes in +fs/4 shift) -> fine LO mix at the intermediate rate (fs_in/2) -> RateConverter -> CF32 output. The halfband stage uses +-1/0 coefficients (no multiplications) and puts the fine LO and the cascade at fs_in/2. That is worth ~1.1-1.7x in a whole receiver (it halves the rate ahead of the polyphase matched filter, so the gain grows with samples/symbol) and close to nothing for the front end alone see the file header for the measurements. Use it because the input IS real. |
| ddcr_state_t * | ddcr_create_matched (double norm_freq, double rate, int pulse, double beta, size_t span, double pulse_sps, size_t num_phases) Create a real-input DDC whose terminal stage IS a matched filter. |
| void | ddcr_destroy (ddcr_state_t * s) Free all resources held by a DDCR instance. Releases the halfband, RateConverter, and LO substructures, then the struct itself. Passing NULL is a no-op. |
| size_t | ddcr_execute (ddcr_state_t * s, const float * in, size_t n_in, float _Complex * out, size_t max_out) Process a block of real float32 samples through the full DDCR signal chain: halfband R2C → LO mix → RateConverter → CF32. The halfband decimates by 2 and applies a built-in +fs/4 frequency shift; the fine NCO then completes the tuning. State is maintained across calls for contiguous streaming. Output length ≈ n_in * rate (±1 from polyphase indexing). A real tone at input normalised frequency f_c has amplitude 0.5 in the baseband output (one-sided spectrum), consistent with analytic signal theory. |
| size_t | ddcr_execute_ctrl (ddcr_state_t * s, const float * x, size_t n_in, double rate_ctrl, double freq_ctrl, float _Complex * out, size_t max_out) Process a real block, steering both control ports. |
| size_t | ddcr_execute_ctrl_max_out (ddcr_state_t * s) As ddcr_execute_max_out() , for the block control-port form. |
| size_t | ddcr_execute_ctrl_push (ddcr_state_t * s, float x, double rate_ctrl, double freq_ctrl, float _Complex * out, size_t max_out) Push ONE real input sample; emit whatever outputs it completes. |
| size_t | ddcr_execute_ctrl_push_max_out (ddcr_state_t * s) Bound for ONE pushed input: ceil(rate) + 1 output periods. Non-zero because the push form has no input block to size from. |
| size_t | ddcr_execute_ctrl_push_tap (ddcr_state_t * s, float x, double rate_ctrl, double freq_ctrl, float _Complex * out, size_t max_out, float _Complex * lo_out, int * n_lo) ddcr_execute_ctrl_push() that also hands back the post-LO sample. |
| size_t | ddcr_execute_ctrl_push_tap2 (ddcr_state_t * s, float x, double rate_ctrl, double freq_ctrl, float _Complex * out, size_t max_out, float _Complex * lo_out, int * n_lo, float _Complex * pre_out, int * n_pre) ddcr_execute_ctrl_push_tap() , plus the MFR-INPUT tap. |
| size_t | ddcr_execute_max_out (ddcr_state_t * s) Upper bound on one execute call's output, or 0 to let the caller size it from the input block (a decimator never exceeds its input). |
| double | ddcr_get_bank_sps (const ddcr_state_t * s) Samples per symbol of the MFR-input tap; a planner outcome. Identical to the complex twin's at every rate ratio — bank_sps is symbol-relative, so the halfband's 2:1 is absorbed by the plan. |
| bool | ddcr_get_clipped (const ddcr_state_t * s) Has the cascade's CIC clipped its input since the last reset? |
| bool | ddcr_get_narrow_pulse (const ddcr_state_t * s) Is this object's rectangular matched filter degenerately narrow? |
| double | ddcr_get_norm_freq (const ddcr_state_t * s) Return the current fine NCO normalised frequency at the intermediate rate (fs_in/2, cycles/sample). |
| double | ddcr_get_rate (const ddcr_state_t * s) Return the total configured rate (fs_out / fs_in, read-only). This is the end-to-end ratio from ADC input to CF32 output. Change it by destroying and recreating the DDCR. |
| void | ddcr_get_state (const ddcr_state_t * s, void * blob) Serialize s's full-chain state intoblob . |
| void | ddcr_reset (ddcr_state_t * s) Zero halfband filter history, LO phase, and resampler history. After reset, the next execute call reproduces the output of the first call after create, enabling repeatable block-by-block tests. |
| size_t | ddcr_run (ddcr_state_t * s, const void * state_in, void * state_out, const float * in, size_t n_in, float _Complex * out, size_t max_out) Pure run: inject state_in , processin , exportstate_out —(state_in, input) -> (state_out, output) over an engine treated as immutable config. Either state may be NULL (NULL in = use current; NULL out = discard).state_in /state_out may alias. |
| void | ddcr_set_norm_freq (ddcr_state_t * s, double norm_freq) Retune the fine NCO without resetting halfband or resampler history. Updates the LO phase increment only; state is preserved for seamless tuning across block boundaries. |
| int | ddcr_set_state (ddcr_state_t * s, const void * blob) Restore full-chain state from blob intos . |
| int | ddcr_set_telemetry (ddcr_state_t * s, dp_tlm_t * tlm, const char * prefix, uint32_t decim) Attach (or detach) a telemetry context on the cascade's AGC. |
| size_t | ddcr_state_bytes (const ddcr_state_t * s) Byte size of s's state blob (envelope + extra + chain). |
Macros¶
| Type | Name |
|---|---|
| define | DDCR_STATE_MAGIC [**DP\_FOURCC**](dp__state_8h.md#define-dp_fourcc) ('D', 'D', 'C', 'R') |
| define | DDCR_STATE_VERSION 1u |
Detailed Description¶
The real-input twin of ddc/ddc_core.h's Ddc: identical from the LO onwards, behind a real-to-complex front end.
float in (fs_in) → halfband R2C (2:1, embedded fs/4 shift)
→ LO mix at intermediate rate (fs_in/2)
→ RateConverter → CF32 out (fs_out)
norm_freq: Fine NCO frequency at the INTERMEDIATE rate (fs_in/2). To tune a real tone at f_carrier (input normalised) to DC: set norm_freq = -(2*f_carrier + 0.5). Total output rate: fs_out = rate * fs_in (rate < 0.5).
The halfband R2C step has an fs/4 frequency shift baked in at zero extra multiplications — the +/-1/0 coefficients multiply for free — and everything after it (the fine LO and the whole cascade) runs at fs_in/2.
What that is worth, measured rather than assumed: for the FRONT END alone, against Ddc fed the same stream promoted to complex, essentially nothing — 1.04x to 1.40x end to end at total rates 0.25/0.125/0.0625, and 0.74x to 1.13x once the real->complex promote is charged to Ddc, with the ratio wandering by block size the way a memory-bound measurement does. The free coefficients are real; multiplies are simply not what this path pays for.
Where the half rate DOES pay is a whole receiver, because it halves the sample rate ahead of the polyphase matched filter: MpskReceiverR against MpskReceiver on the same stream measures 1.13x at sps=20/m_out=8, 1.50x at sps=32/m_out=8 and 1.69x at sps=64/m_out=8. It rises toward 2x with sps (more of the total cost is then pre-MF) but cannot reach it, since both paths fire the same m_out terminal dot products per symbol and those dominate at low sps. Choose DdcR because your input IS real, not for a factor of two.
Like Ddc it has a matched flavor (ddcr_create_matched, Python MatchedDdcr) that puts the pulse on the cascade's terminal stage, and the same two control ports — see ddc/ddc_core.h's file header for what the ports are and why they are duals.
// Tune a real tone at +0.1*fs to DC, decimate by 4
// norm_freq at intermediate rate: -(2 * 0.1 + 0.5) = -0.7
ddcr_state_t *ddcr = ddcr_create(-0.7, 0.25);
float _Complex out[4096];
size_t m = ddcr_execute(ddcr, real_in, 1024, out, 4096);
ddcr_destroy(ddcr);
Public Types Documentation¶
typedef ddcr_state_t¶
DdcR state — the real-to-complex front end, an LO and a cascade.
Do not initialise directly; use ddcr_create() or ddcr_create_matched().
Public Functions Documentation¶
function ddcr_create¶
Create a real-input Digital Down-Converter (Architecture D2). The signal chain is: halfband R2C (2:1, bakes in +fs/4 shift) -> fine LO mix at the intermediate rate (fs_in/2) -> RateConverter -> CF32 output. The halfband stage uses +-1/0 coefficients (no multiplications) and puts the fine LO and the cascade at fs_in/2. That is worth ~1.1-1.7x in a whole receiver (it halves the rate ahead of the polyphase matched filter, so the gain grows with samples/symbol) and close to nothing for the front end alone see the file header for the measurements. Use it because the input IS real.
Parameters:
norm_freqFine NCO frequency at the intermediate rate (fs_in/2, cycles/sample). To tune a real tone at normalised input frequency f_c to DC, set norm_freq = -(2*f_c + 0.5).rateTotal output/input rate. Must be in (0, 0.5) because the halfband pre-decimates by 2.
Returns:
Non-NULL on success, NULL on OOM or invalid args.
>>> from doppler.ddc import Ddcr
>>> ddcr = Ddcr(norm_freq=-0.7, rate=0.25)
>>> ddcr.norm_freq
-0.7
>>> ddcr.rate
0.25
function ddcr_create_matched¶
Create a real-input DDC whose terminal stage IS a matched filter.
ddcr_state_t * ddcr_create_matched (
double norm_freq,
double rate,
int pulse,
double beta,
size_t span,
double pulse_sps,
size_t num_phases
)
The matched flavor of DdcR (Python: MatchedDdcr), and identical to ddc_create_matched() from the LO onwards — the halfband R2C front end is a fixed 2:1 integer stage, so the pulse still lands on the cascade's terminal stage and both control ports mean exactly what they mean there.
Note the rate arithmetic the halfband imposes: the cascade behind it runs at 2*rate, and this function does that on the caller's behalf, so a caller wanting m outputs per symbol still passes the TOTAL rate = m/sps. pulse_sps is in output samples, so the front end does not affect it.
Parameters:
norm_freqFine NCO frequency at the INTERMEDIATE rate (fs_in/2) — the same reference ddcr_create() uses.rateTotal output/input rate; must be in (0, 0.5).pulseRC_PULSE_RRC / RC_PULSE_IANDD (RC_PULSE_NONE is invalid here — use ddcr_create()).betaRRC roll-off in[0, 1](ignored for the rectangle).spanOne-sided RRC span in symbols (ignored for the rectangle).pulse_spsThe pulse's period in output samples.num_phasesTerminal-stage arms; a power of two.
Returns:
Non-NULL on success, NULL on a bad parameter or OOM.
>>> from doppler.ddc import MatchedDdcr
>>> rx = MatchedDdcr(norm_freq=-0.6875, rate=2 / 16, pulse="rrc")
>>> rx.rate
0.125
function ddcr_destroy¶
Free all resources held by a DDCR instance. Releases the halfband, RateConverter, and LO substructures, then the struct itself. Passing NULL is a no-op.
>>> from doppler.ddc import Ddcr
>>> ddcr = Ddcr(norm_freq=0.0, rate=0.25)
>>> ddcr.close() # releases C memory immediately
function ddcr_execute¶
Process a block of real float32 samples through the full DDCR signal chain: halfband R2C → LO mix → RateConverter → CF32. The halfband decimates by 2 and applies a built-in +fs/4 frequency shift; the fine NCO then completes the tuning. State is maintained across calls for contiguous streaming. Output length ≈ n_in * rate (±1 from polyphase indexing). A real tone at input normalised frequency f_c has amplitude 0.5 in the baseband output (one-sided spectrum), consistent with analytic signal theory.
size_t ddcr_execute (
ddcr_state_t * s,
const float * in,
size_t n_in,
float _Complex * out,
size_t max_out
)
Parameters:
sMust be non-NULL.inReal float32 input block.n_inNumber of input samples (C-only, hidden from Python).outCF32 output buffer (C-only, hidden from Python).max_outOutput buffer capacity (C-only, hidden from Python).
Returns:
Number of output samples written (C-only).
>>> from doppler.ddc import Ddcr
>>> import numpy as np
>>> ddcr = Ddcr(norm_freq=-0.7, rate=0.25)
>>> t = np.arange(4096)
>>> x = np.cos(2 * np.pi * 0.1 * t).astype(np.float32)
>>> out = np.empty(len(x), dtype=np.complex64)
>>> y = ddcr.execute(x, out)
>>> y.shape
(1024,)
>>> y.dtype
dtype('complex64')
>>> round(float(abs(y[500])), 2) # analytic signal of a unit cosine
1.0
function ddcr_execute_ctrl¶
Process a real block, steering both control ports.
size_t ddcr_execute_ctrl (
ddcr_state_t * s,
const float * x,
size_t n_in,
double rate_ctrl,
double freq_ctrl,
float _Complex * out,
size_t max_out
)
The control-port form of ddcr_execute(); see ddc_execute_ctrl() for the semantics, which are identical except for where the LO lives.
Parameters:
sMust be non-NULL.xReal float32 input block.n_inNumber of input samples.rate_ctrlRate deviation added to the terminal Resampler stage's rate (referenced to the terminal, post-decimation rate).freq_ctrlFrequency deviation added to the fine LO, in cycles/sample at the INTERMEDIATE rate (fs_in/2) — the halfband has already decimated by two by the time the mix happens, so a discriminator working in cycles per ADC sample must be doubled before it lands here.outCF32 output buffer.max_outCapacity ofoutin samples.
Returns:
Number of output samples written.
>>> from doppler.ddc import Ddcr
>>> import numpy as np
>>> ddcr = Ddcr(norm_freq=-0.5, rate=0.25) # LO 0.2 short of tune
>>> t = np.arange(4096)
>>> x = np.cos(2 * np.pi * 0.1 * t).astype(np.float32)
>>> y = ddcr.execute_ctrl(x, 0.0, -0.2) # ctrl completes the tune
>>> y.shape
(1024,)
>>> round(float(abs(y[100:].mean())), 2) # real tone -> DC, amp 1.0
1.0
function ddcr_execute_ctrl_max_out¶
As ddcr_execute_max_out() , for the block control-port form.
function ddcr_execute_ctrl_push¶
Push ONE real input sample; emit whatever outputs it completes.
size_t ddcr_execute_ctrl_push (
ddcr_state_t * s,
float x,
double rate_ctrl,
double freq_ctrl,
float _Complex * out,
size_t max_out
)
The per-input streaming form of ddcr_execute_ctrl(), for a closed loop. The halfband consumes two inputs per intermediate sample, so every other push does no mixing and emits nothing at all — the LO advances (and its control is applied) once per intermediate sample, which is the rate the LO runs at.
Parameters:
sMust be non-NULL.xOne real float32 input sample.rate_ctrlRate deviation for this input (terminal-stage rate).freq_ctrlFrequency deviation, cycles/sample at fs_in/2.outOutput buffer for any emitted samples.max_outCapacity ofout.
Returns:
Number of outputs written (0, 1, or more).
>>> from doppler.ddc import Ddcr
>>> import numpy as np
>>> ddcr = Ddcr(norm_freq=-0.7, rate=0.25)
>>> x = np.cos(2 * np.pi * 0.1 * np.arange(128)).astype(np.float32)
>>> outs = [ddcr.execute_ctrl_push(float(s), 0.0, 0.0) for s in x]
>>> int(sum(len(o) for o in outs)) # 128 real inputs, rate 1/4 -> 32
32
>>> [len(o) for o in outs[:4]] # halfband: 0 until a strobe
[0, 0, 0, 1]
function ddcr_execute_ctrl_push_max_out¶
Bound for ONE pushed input: ceil(rate) + 1 output periods. Non-zero because the push form has no input block to size from.
function ddcr_execute_ctrl_push_tap¶
ddcr_execute_ctrl_push() that also hands back the post-LO sample.
size_t ddcr_execute_ctrl_push_tap (
ddcr_state_t * s,
float x,
double rate_ctrl,
double freq_ctrl,
float _Complex * out,
size_t max_out,
float _Complex * lo_out,
int * n_lo
)
The real-input twin of ddc_execute_ctrl_push_tap(); see that function for why the tap exists (a carrier discriminator's unambiguous range is set by the rate it updates at, so a caller may want the widest, least-filtered stream rather than the cleanest one).
The one difference is that this front end does NOT mix every input: the 2:1 halfband consumes two real inputs per intermediate sample, so n_lo comes back 0 on every other push and lo_out is untouched. The tapped stream therefore runs at fs_in/2, the LO's own rate — half as fast as the complex twin's for the same nominal sps, which halves this tap's frequency range in input-referred terms exactly as it halves everything else the LO sees.
Parameters:
sMust be non-NULL.xOne real float32 input sample.rate_ctrlRate deviation for this input (terminal-stage rate).freq_ctrlFrequency deviation, cycles/sample at fs_in/2.outOutput buffer for any emitted outputs.max_outCapacity ofout.lo_outReceives the post-LO, pre-cascade sample whenn_locomes back 1. May be NULL.n_loReceives 1 when the halfband fired for this input and the LO stepped, 0 otherwise. May be NULL.
Returns:
Number of terminal outputs written (0, 1, or more).
function ddcr_execute_ctrl_push_tap2¶
ddcr_execute_ctrl_push_tap() , plus the MFR-INPUT tap.
size_t ddcr_execute_ctrl_push_tap2 (
ddcr_state_t * s,
float x,
double rate_ctrl,
double freq_ctrl,
float _Complex * out,
size_t max_out,
float _Complex * lo_out,
int * n_lo,
float _Complex * pre_out,
int * n_pre
)
The real-input twin of ddc_execute_ctrl_push_tap2(). pre_out receives the cascade's output after every integer stage and after the AGC but ahead of the terminal matched filter — the node an NDA carrier discriminator can read with no symbol timing. Its rate is ddcr_get_bank_sps() samples per symbol.
The halfband gates the whole call: on the inputs it swallows there is no LO step and no cascade push, so n_lo and n_pre both come back 0.
Parameters:
sMust be non-NULL.xOne real input sample.rate_ctrlRate deviation for this input (terminal-stage rate).freq_ctrlFrequency deviation, cycles/sample at the LO's own (halved) intermediate rate.outOutput buffer for any emitted outputs.max_outCapacity ofout.lo_outReceives the post-LO, pre-cascade sample whenn_locomes back 1. May be NULL.n_loReceives 1 when the halfband emitted and the LO stepped, else 0. May be NULL.pre_outReceives the MFR-input sample; may be NULL.n_preReceives 1 ifpre_outwas written, else 0; may be NULL.
Returns:
Number of terminal outputs written.
function ddcr_execute_max_out¶
Upper bound on one execute call's output, or 0 to let the caller size it from the input block (a decimator never exceeds its input).
function ddcr_get_bank_sps¶
Samples per symbol of the MFR-input tap; a planner outcome. Identical to the complex twin's at every rate ratio — bank_sps is symbol-relative, so the halfband's 2:1 is absorbed by the plan.
function ddcr_get_clipped¶
Has the cascade's CIC clipped its input since the last reset?
Forwarded from RateConverter_get_clipped(); see ddc_get_clipped(). The halfband R2C front end has unity passband gain and a real tone lands at amplitude 0.5 in the analytic output, so a full-scale ADC stream sits comfortably inside the CIC's bound — but a scaled-up input does not.
function ddcr_get_narrow_pulse¶
Is this object's rectangular matched filter degenerately narrow?
The real chain's copy of ddc_get_narrow_pulse(): true only for the matched flavor with pulse = RC_PULSE_IANDD and fewer than four output samples per symbol, where the one-symbol-wide rectangle's matched filter is a 2-3 tap sum. Construction also raises a UserWarning.
function ddcr_get_norm_freq¶
Return the current fine NCO normalised frequency at the intermediate rate (fs_in/2, cycles/sample).
function ddcr_get_rate¶
Return the total configured rate (fs_out / fs_in, read-only). This is the end-to-end ratio from ADC input to CF32 output. Change it by destroying and recreating the DDCR.
function ddcr_get_state¶
Serialize s's full-chain state intoblob .
function ddcr_reset¶
Zero halfband filter history, LO phase, and resampler history. After reset, the next execute call reproduces the output of the first call after create, enabling repeatable block-by-block tests.
>>> from doppler.ddc import Ddcr
>>> import numpy as np
>>> ddcr = Ddcr(norm_freq=0.0, rate=0.25)
>>> x = np.ones(64, dtype=np.float32)
>>> out = np.empty(64, dtype=np.complex64)
>>> y1 = ddcr.execute(x, out).copy()
>>> ddcr.reset()
>>> y2 = ddcr.execute(x, out)
>>> bool(np.array_equal(y1, y2))
True
function ddcr_run¶
Pure run: inject state_in , processin , exportstate_out —(state_in, input) -> (state_out, output) over an engine treated as immutable config. Either state may be NULL (NULL in = use current; NULL out = discard).state_in /state_out may alias.
size_t ddcr_run (
ddcr_state_t * s,
const void * state_in,
void * state_out,
const float * in,
size_t n_in,
float _Complex * out,
size_t max_out
)
Returns:
Number of CF32 output samples written.
function ddcr_set_norm_freq¶
Retune the fine NCO without resetting halfband or resampler history. Updates the LO phase increment only; state is preserved for seamless tuning across block boundaries.
Parameters:
sMust be non-NULL.norm_freqNew frequency at the intermediate rate (fs_in/2).
>>> from doppler.ddc import Ddcr
>>> ddcr = Ddcr(norm_freq=-0.7, rate=0.25)
>>> ddcr.norm_freq = -0.5
>>> ddcr.norm_freq
-0.5
function ddcr_set_state¶
Restore full-chain state from blob intos .
Returns:
DP_OK, or DP_ERR_INVALID if the envelope/rate disagree with s (rebuild the engine from the matching descriptor first).
function ddcr_set_telemetry¶
Attach (or detach) a telemetry context on the cascade's AGC.
The twin of ddc_set_telemetry(), forwarded to the same RateConverter_set_telemetry() over the same cascade: the R2C front end and the fixed stages have no loop to report, so the one instrumented child is the pre-terminal AGC ("<prefix>.gain_db" and "<prefix>.level_db"). DP_OK with no probes when the cascade has no AGC enabled.
Parameters:
sMust be non-NULL.tlmTelemetry context to attach, or NULL to detach.prefixProbe-name prefix, e.g. "rx.agc".decimEmit every decim-th gain update; >= 1.
Returns:
DP_OK, or DP_ERR_INVALID when the probe table cannot take the AGC's probes (the attach fails whole).
function ddcr_state_bytes¶
Byte size of s's state blob (envelope + extra + chain).
Macro Definition Documentation¶
define DDCR_STATE_MAGIC¶
define DDCR_STATE_VERSION¶
The documentation for this class was generated from the following file native/inc/ddcr/ddcr_core.h