File cic_core.h¶
FileList > cic > cic_core.h
Go to the source code of this file
CIC decimation filter — 4-stage, M=1, UQ16 integer pipeline. More...
#include "doppler/clib_common.h"#include "doppler/dp_state.h"#include "doppler/jm_perf.h"
Classes¶
| Type | Name |
|---|---|
| struct | dp_cic_state_t CIC filter state. |
Public Functions¶
| Type | Name |
|---|---|
| dp_cic_state_t * | dp_cic_create (uint32_t R) Create a 4-stage, M=1 CIC decimation filter. Allocates the state struct on the heap and pre-computes the normalisation right-shift (CIC_N * log2(R) bits). All integrator and comb accumulators are zeroed; the first output arrives after R input samples. Returns NULL for invalid R or OOM. Input amplitude is bounded: |Re| and |Im| <= 2.0. A component beyond +-2.0 is clipped at the boundary before any filtering; the sample stream gives no sign of it, so check the sticky clipped flag. Unlike doppler's floating-point blocks this one is not scale-free scale the input into range first. |
| double | dp_cic_dc_gain (const dp_cic_state_t * state) The filter's response to a constant input, from its own geometry. |
| JM_FORCEINLINE JM_HOT size_t | dp_cic_decimate (dp_cic_state_t * state, const float _Complex * in, size_t n_in, float _Complex * out, size_t max_out) Decimate a block of CF32 samples through the CIC pipeline. Each sample is converted to offset-binary UQ16, pushed through CIC_N integrators (unsigned wrapping), and when the phase counter reaches R the integrated value is passed through CIC_N M=1 comb stages and converted back to CF32. State persists between calls. Feeding blocks that are multiples of R gives predictable output counts (exactly n_in/R samples per block). |
| size_t | dp_cic_decimate_max_out (dp_cic_state_t * state) Upper bound on decimate output — returns 0 (lazy-alloc signal). |
| void | dp_cic_destroy (dp_cic_state_t * state) Free the filter state. |
| void | dp_cic_get_state (const dp_cic_state_t * state, void * blob) Serialize R, the integrator/comb/phase state and clipped intoblob . shift is not written: it is derived from R. |
| void | dp_cic_reconfigure (dp_cic_state_t * state, uint32_t R) Change the decimation ratio in place and reset all filter state. Recomputes the normalisation shift (CIC_N * log2(R)) and zeros all accumulators so the filter behaves exactly like a freshly created one with the new R. Silently ignores R values that are not a power-of-two in [2, 2048] (CIC_R_MAX ) — the state is left unchanged in that case. |
| void | dp_cic_reset (dp_cic_state_t * state) Zero all integrator and comb accumulators; preserve R and shift. The first output sample after reset arrives after R more input samples, matching post-create behaviour. Use between signal bursts to eliminate transient artefacts caused by residual pipeline state. Also clears the sticky clipped flag. |
| int | dp_cic_set_state (dp_cic_state_t * state, const void * blob) Restore R, the integrator/comb/phase state and clipped fromblob . |
| size_t | dp_cic_state_bytes (const dp_cic_state_t * state) Bytes dp_cic_get_state() writes (envelope + payload). |
Macros¶
| Type | Name |
|---|---|
| define | CIC_N 4 |
| define | CIC_PAPR_HEADROOM 2.0fPeak-to-average headroom the input encoding reserves, as a voltage ratio (2.0 = 6 dB). |
| define | CIC_R_MAX 2048uLargest decimation ratio a CIC will be built at. |
| define | CIC_STATE_MAGIC [**DP\_FOURCC**](dp__state_8h.md#define-dp_fourcc) ('C', 'I', 'C', '\_') |
| define | CIC_STATE_VERSION 3u |
Detailed Description¶
INPUT AMPLITUDE IS BOUNDED: |Re| and |Im| <= 2.0. A component beyond that is CLIPPED at the boundary, before any filtering happens. Unlike the library's floating-point blocks this one is not scale-free — it is the one place where turning the input gain up changes the answer — and the clip is silent in the sample stream: no error, no NaN, just a degraded output that looks plausible. Measured cost: an RRC-BPSK waveform driven into the clip matched-filters to -25 dB EVM where the same waveform well inside it reaches -50 dB.
The bound is CIC_PAPR_HEADROOM (2.0, i.e. 6 dB) and not 1.0 because that headroom is exactly what it buys: the encode scale is 32768 / CIC_PAPR_HEADROOM, so full scale sits 6 dB above unity amplitude and a signal whose PEAKS exceed its unit average — every pulse-shaped waveform — has somewhere to put them. Budgeting the DC gain alone left the peaks clipping against a bound the average never approached.
**So check clipped** — a sticky flag raised by any saturating component and cleared by dp_cic_reset(), following the same convention as the quantizing cvt converters (adc, f32_to_uq15, ...). It is free: the four boundary comparisons run on every sample regardless, so recording that one fired costs a register OR. There is no reason to run a CIC without checking it at least once against real input.
(Why the input is bounded at all: the pipeline is integer, so the CF32 boundary is quantized. That is an implementation detail — the input constraint above is the whole of what a caller needs. See docs/design/QUANTIZATION.md for the encoding and the headroom budget.)
Fixed design parameters: N = 4 stages (~77 dB alias rejection at f_p = 0.1 * f_out) M = 1 (differential delay — one-sample comb) R = power-of-two decimation ratio (enforced at create time)
Input/output boundary: CF32 (float _Complex), matching the doppler default signal type. Internally, each sample is converted to UQ16 — offset-binary: v_q15 + 32768 → [0, 65535] in a uint64_t — giving 48 bits of headroom for the pipeline gain of N * log2(R) bits. At the cap CIC_R_MAX (2048, log2 = 11) the gain is 44 bits; max accumulation = 65535 * R^N = (2^16 - 1) * 2^44, which is 16x inside 2^64. R = 4096 also fits, but to within one part in 65536 — see CIC_R_MAX for why the cap is a halving below it.
All arithmetic is unsigned: inputs are non-negative [0, 65535], wrapping is defined (mod 2^64), and the output decode subtracts the offset in floating-point — no signed integer casts anywhere in the hot path.
The unsigned modular-arithmetic CIC property guarantees exact outputs: every intermediate overflow in the integrators cancels in the comb stages, provided the true result fits in 64 bits. So the integrator/comb pipeline itself needs no saturation, no range checks and no floating-point — the one saturation in the block is at the CF32 encoder, and it is the +-2.0 input bound described at the top of this file, not an arithmetic guard.
With M=1 and N fixed, the entire comb state is four uint64_t values per channel — no heap allocation beyond the state struct itself.
Alias rejection : ~77 dB at f_p = 0.1 * f_out (independent of R) Passband droop : ~0.57 dB at f_p = 0.1 * f_out (independent of R) Output precision: 16-bit Q15 (independent of R and N)
dp_cic_state_t *cic = dp_cic_create(16); // R=16, N=4, M=1
size_t n_out = dp_cic_decimate(cic, in, 1024, out, 1024);
dp_cic_destroy(cic);
Public Functions Documentation¶
function dp_cic_create¶
Create a 4-stage, M=1 CIC decimation filter. Allocates the state struct on the heap and pre-computes the normalisation right-shift (CIC_N * log2(R) bits). All integrator and comb accumulators are zeroed; the first output arrives after R input samples. Returns NULL for invalid R or OOM. Input amplitude is bounded: |Re| and |Im| <= 2.0. A component beyond +-2.0 is clipped at the boundary before any filtering; the sample stream gives no sign of it, so check the sticky clipped flag. Unlike doppler's floating-point blocks this one is not scale-free scale the input into range first.
Parameters:
RDecimation ratio. Must be a power of two in[2, 2048](CIC_R_MAX). Returns NULL for R=0, non-power-of-two, or a ratio above that cap.
Returns:
Heap-allocated state, or NULL on invalid R or OOM.
function dp_cic_dc_gain¶
The filter's response to a constant input, from its own geometry.
A CIC's pipeline gain is R^N, and this implementation removes it with a right-shift of N*log2(R) bits, so the DC gain is R^N / 2^shift — one exactly, whenever the shift matches R. Computed from R and the stored shift rather than measured, so a mismatch between the two is visible without running a signal through the filter.
Parameters:
stateState. Must be non-NULL.
Returns:
The DC gain. 1.0 for every power-of-two R the filter accepts.
dp_cic_state_t *c = dp_cic_create (32);
printf ("%.4f\n", dp_cic_dc_gain (c)); // 1.0000
dp_cic_destroy (c);
function dp_cic_decimate¶
Decimate a block of CF32 samples through the CIC pipeline. Each sample is converted to offset-binary UQ16, pushed through CIC_N integrators (unsigned wrapping), and when the phase counter reaches R the integrated value is passed through CIC_N M=1 comb stages and converted back to CF32. State persists between calls. Feeding blocks that are multiples of R gives predictable output counts (exactly n_in/R samples per block).
JM_FORCEINLINE JM_HOT size_t dp_cic_decimate (
dp_cic_state_t * state,
const float _Complex * in,
size_t n_in,
float _Complex * out,
size_t max_out
)
Note:
Input amplitude is bounded: |Re| and |Im| <= 2.0. A component beyond +-2.0 is clipped at the boundary before filtering; the sample stream gives no sign of it, so check the sticky clipped flag. Scale the input into range first; see the file header.
Parameters:
statePointer to a valid dp_cic_state_t.inCF32 input block, |Re| and |Im| <= 2.0 (clipped otherwise).n_inNumber of input samples.outOutput buffer; must hold at least max_out elements.max_outCapacity ofoutin samples. Normally n_in (the loosest bound: at most one output per input). If it is smaller the integrators and combs still advance over every input sample the pipeline is a running filter and cannot be left half-fed but emission stops, so the samples past the capacity are dropped rather than written past the end.
Returns:
Number of CF32 samples written to out: min(floor((phase + n_in) / R), max_out), where phase is the input count carried from the previous call.
>>> from doppler.resample import CIC
>>> import numpy as np
>>> cic = CIC(R=16)
>>> for _ in range(4):
... _ = cic.decimate(np.zeros(16, dtype=np.complex64))
>>> y = cic.decimate(np.zeros(16, dtype=np.complex64))
>>> y.tolist(), y.dtype
([0j], dtype('complex64'))
function dp_cic_decimate_max_out¶
Upper bound on decimate output — returns 0 (lazy-alloc signal).
The Python extension allocates n_in elements on the first call. Since n_in >= ceil(n_in/R) = n_out for all R >= 1, the buffer is always large enough as long as block size stays consistent.
Parameters:
stateCIC state (unused).
Returns:
Always 0, meaning "no fixed cap": size the output to the input length.
function dp_cic_destroy¶
Free the filter state.
Parameters:
stateState from dp_cic_create(); NULL is a no-op.
function dp_cic_get_state¶
Serialize R, the integrator/comb/phase state and clipped intoblob . shift is not written: it is derived from R.
Parameters:
statePointer to a valid dp_cic_state_t.blobDestination; at least dp_cic_state_bytes() bytes.
function dp_cic_reconfigure¶
Change the decimation ratio in place and reset all filter state. Recomputes the normalisation shift (CIC_N * log2(R)) and zeros all accumulators so the filter behaves exactly like a freshly created one with the new R. Silently ignores R values that are not a power-of-two in [2, 2048] (CIC_R_MAX ) — the state is left unchanged in that case.
Parameters:
statePointer to a valid dp_cic_state_t.RNew decimation ratio. Same constraints as dp_cic_create().
>>> from doppler.resample import CIC
>>> cic = CIC(R=4)
>>> cic.reconfigure(8)
>>> cic.R, cic.shift
(8, 12)
function dp_cic_reset¶
Zero all integrator and comb accumulators; preserve R and shift. The first output sample after reset arrives after R more input samples, matching post-create behaviour. Use between signal bursts to eliminate transient artefacts caused by residual pipeline state. Also clears the sticky clipped flag.
Parameters:
statePointer to a valid dp_cic_state_t.
function dp_cic_set_state¶
Restore R, the integrator/comb/phase state and clipped fromblob .
R is the writer's, as dp_cic_reconfigure() would set it, and shift is recomputed from it, so a blob restores into a CIC built with any R (doppler#2642). A blob whose R is not one dp_cic_create() accepts, or whose phase is not below that R, is refused and state is unchanged.
Parameters:
statePointer to a valid dp_cic_state_t.blobA blob from dp_cic_get_state().
Returns:
DP_OK, or DP_ERR_INVALID if the envelope rejects, R is invalid or the phase is not below R.
function dp_cic_state_bytes¶
Bytes dp_cic_get_state() writes (envelope + payload).
Parameters:
stateCIC state (unused: the size is fixed).
Returns:
sizeof(dp_state_hdr_t) + 4 + 4 * CIC_N * 8 + 4 + 1 bytes, the same for every R.
Macro Definition Documentation¶
define CIC_N¶
Fixed stage count. Alias rejection ~19.2 dB/stage at f_p=0.1.
define CIC_PAPR_HEADROOM¶
Peak-to-average headroom the input encoding reserves, as a voltage ratio (2.0 = 6 dB).
A fixed-point CIC has TWO input-budget terms, and only one of them is the accumulator's. The DC gain R^N is budgeted there — 16-bit input plus 48 bits of pipeline gain would fill the 64-bit accumulator exactly at R = 4096, which is why CIC_R_MAX sits a halving below it. The PAPR is budgeted here, at the encoder, because a signal's peak is not its symbol amplitude: a root-raised-cosine symbol stream peaks at 1.582x its symbol amplitude (measured, and a pulse property — identical at every samples-per-symbol).
Encoding at full scale therefore clipped any signal presented at its natural amplitude, and the caller had to back off by the PAPR — 4 dB that nothing downstream restored, leaving a timing loop under-driven by the square of it, 2.5x. Reserving the headroom here instead lets a unit- amplitude signal through unclipped and costs 2 dB of quantisation SNR against a caller who backs off perfectly, which no caller did.
This changes only the encode/decode scale PAIR, never the normalising shift, so the DC gain stays exactly one — see dp_cic_dc_gain().
define CIC_R_MAX¶
Largest decimation ratio a CIC will be built at.
The 64-bit accumulator holds 65535 * R^CIC_N. At R = 4096 that is (2^16 - 1) * 2^48 = 2^64 - 2^48 — it fits, and fills the accumulator to within one part in 65536. "Fits exactly" is not headroom: it is the value at which any further term overflows, and the CIC's exactness argument (every intermediate overflow cancels in the combs) holds only while the TRUE result fits in 64 bits.
2048 gives 2^60 - 2^44 — 16x margin — for one halving of the largest single-stage ratio. Nothing in the tree asked for more: the planner is the only thing that can reach the cap, and past it it already hands the residual to the resampler stage, so a lower cap costs a slightly larger residual and nothing else.
define CIC_STATE_MAGIC¶
FourCC type tag ("CIC_") stamped in a CIC state blob's header.
define CIC_STATE_VERSION¶
Layout version of the CIC state blob (2 added clipped, 3 added R).
The documentation for this class was generated from the following file native/inc/doppler/cic/cic_core.h