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 | cic_state_t CIC filter state. |
Public Functions¶
| Type | Name |
|---|---|
| cic_state_t * | 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| <= 1.0. A component beyond +-1.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 | cic_dc_gain (const cic_state_t * state) The filter's response to a constant input, from its own geometry. |
| JM_FORCEINLINE JM_HOT size_t | cic_decimate (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 | cic_decimate_max_out (cic_state_t * state) Upper bound on decimate output — returns 0 (lazy-alloc signal). |
| void | cic_destroy (cic_state_t * state) |
| void | cic_get_state (const cic_state_t * state, void * blob) Serialize the integrator/comb/phase state into blob . |
| void | cic_reconfigure (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 | cic_reset (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. |
| int | cic_set_state (cic_state_t * state, const void * blob) Restore the integrator/comb/phase state from blob . |
| size_t | cic_state_bytes (const cic_state_t * state) Bytes 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 2u |
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 only by 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 +-1.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)
cic_state_t *cic = cic_create(16); // R=16, N=4, M=1
size_t n_out = cic_decimate(cic, in, 1024, out, 1024);
cic_destroy(cic);
Public Functions Documentation¶
function 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| <= 1.0. A component beyond +-1.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 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.
function 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 cic_decimate (
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| <= 1.0. A component beyond +-1.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 cic_state_t.inCF32 input block, |Re| and |Im| <= 1.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:
CF32 output array; length is min(floor((phase + n_in) / R), max_out).
>>> 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 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.
function cic_destroy¶
Free resources. NULL is a no-op.
function cic_get_state¶
Serialize the integrator/comb/phase state into blob .
function 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 cic_state_t.RNew decimation ratio. Same constraints as cic_create().
>>> from doppler.resample import CIC
>>> cic = CIC(R=4)
>>> cic.reconfigure(8)
>>> cic.R, cic.shift
(8, 12)
function 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.
function cic_set_state¶
Restore the integrator/comb/phase state from blob .
Returns:
DP_OK, or DP_ERR_INVALID if the blob's envelope rejects.
function cic_state_bytes¶
Bytes cic_get_state() writes (envelope + payload).
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 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¶
define CIC_STATE_VERSION¶
The documentation for this class was generated from the following file native/inc/doppler/cic/cic_core.h