Skip to content

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.0f
Peak-to-average headroom the input encoding reserves, as a voltage ratio (2.0 = 6 dB).
define CIC_R_MAX 2048u
Largest 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.

cic_state_t * cic_create (
    uint32_t R
) 

Parameters:

  • R Decimation 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.

>>> from doppler.resample import CIC
>>> cic = CIC(R=16)
>>> cic.R, cic.shift
(16, 16)

function cic_dc_gain

The filter's response to a constant input, from its own geometry.

double cic_dc_gain (
    const cic_state_t * state
) 

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:

  • state State. Must be non-NULL.

Returns:

The DC gain. 1.0 for every power-of-two R the filter accepts.

cic_state_t *c = cic_create (32);
printf ("%.4f\n", cic_dc_gain (c));   // 1.0000
cic_destroy (c);

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:

  • state Pointer to a valid cic_state_t.
  • in CF32 input block, |Re| and |Im| <= 1.0 (clipped otherwise).
  • n_in Number of input samples.
  • out Output buffer; must hold at least max_out elements.
  • max_out Capacity of out in 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).

size_t cic_decimate_max_out (
    cic_state_t * state
) 

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

void cic_destroy (
    cic_state_t * state
) 

Free resources. NULL is a no-op.


function cic_get_state

Serialize the integrator/comb/phase state into blob .

void cic_get_state (
    const cic_state_t * state,
    void * 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.

void cic_reconfigure (
    cic_state_t * state,
    uint32_t R
) 

Parameters:

>>> 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.

void cic_reset (
    cic_state_t * state
) 

>>> from doppler.resample import CIC
>>> cic = CIC(R=16)
>>> cic.reset()
>>> cic.R
16

function cic_set_state

Restore the integrator/comb/phase state from blob .

int cic_set_state (
    cic_state_t * state,
    const void * 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).

size_t cic_state_bytes (
    const cic_state_t * state
) 


Macro Definition Documentation

define CIC_N

#define CIC_N `4`

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).

#define CIC_PAPR_HEADROOM `2.0f`

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.

#define CIC_R_MAX `2048u`

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_MAGIC `DP_FOURCC ('C', 'I', 'C', '_')`

define CIC_STATE_VERSION

#define CIC_STATE_VERSION `2u`


The documentation for this class was generated from the following file native/inc/doppler/cic/cic_core.h