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 "clib_common.h"#include "dp_state.h"#include "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, 4096] — 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_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. For R <= 4096 (log2 = 12) the gain is 48 bits; max accumulation = 65535 * R^N = (2^16 - 1) * 2^48 = 2^64 - 2^48 < 2^64, so no overflow occurs.
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, 4096]. Returns NULL for R=0, non-power-of-two, or R > 4096.
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, 4096] — 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 fills the 64-bit accumulator exactly at R = 4096. 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_STATE_MAGIC¶
define CIC_STATE_VERSION¶
The documentation for this class was generated from the following file native/inc/cic/cic_core.h