A fixed-point implementation of a decimating CIC (Cascaded Integrator–Comb) filter, written in C and designed to explicitly model how it would be implemented on a resource-constrained 8-bit microcontroller.
The project includes:
- a header-only C implementation using byte-wise arithmetic,
- a small command-line interface (CLI) for running the filter on input data,
- a deterministic test-vector pipeline,
- an independent Python reference model,
- bit-exact verification,
- and frequency-domain analysis (magnitude response and −3 dB bandwidth plot).
This implementation was originally developed as part of a bachelor’s project on low-power sigma-delta ADC design and has since been cleaned up and generalized for standalone use.
-
Header-only C implementation
All logic lives in
cic_decimator.h, suitable for embedded integration and with minimal dependencies (stdint.handstring.honly). -
Explicit 8-bit arithmetic model
Internal state is stored as byte arrays with explicit carry/borrow handling. Arithmetic wraps modulo 2^B (no saturation), matching typical MCU behavior.
-
Compile-time configurable parameters
- Number of stages (N)
- Decimation factor (R)
- Differential delay (M)
- Internal word width (e.g. 24-bit, 32-bit)
-
CLI wrapper for easy testing with input/output files
Compile and run the filter on input sample data from a file, with processed output written to another file.
-
Deterministic verification
- Fixed test vectors (impulse, step, sine, random)
- Bit-exact comparison against an independent Python integer reference model
-
Frequency-domain analysis
- Magnitude response measurement via sine sweep
- Theoretical magnitude response calculation for comparison
- −3 dB bandwidth estimation
.
├── src/
│ ├── cic_decimator.h # Header-only CIC implementation
│ ├── cic_config.h # Compile-time parameter configuration
│ └── cic_cli.c # Small CLI wrapper for running the filter
│
├── python/
│ ├── cic_reference.py # Integer reference model
│ ├── verify_bitexact.py # Bit-exact verification against C implementation
│ ├── analyze_cic.py # Frequency-domain analysis & plots
│ └── cic_config_from_header.py # Reads parameters from cic_config.h
│
├── tests/
│ └── gen_vectors.py # Test vector generator
│
├── docs/
│ ├── cic_block_diagram.svg
│ └── magnitude_response_sinesweep_norm.png
│
├── Makefile
├── requirements.txt
├── LICENSE
└── README.md
The CIC decimator implements the standard cascaded integrator–comb structure configured for decimation. It consists of three main sections:
- N integrator stages running at the input sample rate (Fs_in)
- Downsampling by a factor of R
- N comb stages with a differential delay of M running at the output sample rate (Fs_out = Fs_in / R)
The block diagram above illustrates the structure of a general second-order (N=2) decimating CIC filter with arbitrary decimation factor R and differential delay M. The 'sign-extend' block on the input side represents the sign-extension applied to the input samples to match the word width of the integrator and comb stages.
The parameters of the CIC decimator are configured at compile time and defined in src/cic_config.h as follows:
#define CIC_N 2 // Number of stages (filter order)
#define CIC_R 128 // Decimation factor
#define CIC_M 1 // Differential delay
#define CIC_WORD_BITS 24 // Internal word widthThe default values used here are N=2, R=128, M=1, with a 24-bit internal word width.
The included Python scripts read these values to make sure that analysis and verification always match the C build configuration.
The current implementation assumes a signed 10-bit two’s-complement input signal, packed into the lower 10 bits of a 16-bit value.
- Valid input range: −512 … +511
- Internally, input samples are sign-extended to the configured internal word width (e.g. 24-bit) before entering the integrator chain.
- This choice reflects a typical sigma-delta ADC backend or low-resolution quantizer feeding a high-resolution decimation stage.
The input packing and sign-extension logic is isolated and can be adapted to other input resolutions with minimal changes if required. For a given input resolution B_in, the required internal word width (and output word width) B_out can be calculated as:
B_out = B_in + N * log2(R * M)
where B_in is the input bit width. In this case with B_in = 10 bits and the chosen default values of N, R, and M as defined above (2, 128, and 1), this results in:
B_out = 10 + 2 * log2(128 * 1) = 10 + 2 * 7 = 24 bits
The magnitude response below shows the measured baseband response of the CIC decimator (using a sine sweep at the input rate), normalized to DC, together with the theoretical CIC response.
- C compiler (tested with
clang) - Python 3.10+ (optional, only needed for verification and analysis scripts)
To install the required Python packages, run:
pip install -r requirements.txt
To compile the CLI executable, run:
make
The CLI expects a text file with one signed integer per line (range: [-512, 511]) as well as an output file path. Example usage:
./build/cic_cli input.txt output.txt
The make vectors target can be used to generate test vectors in the tests/ directory. These include impulse, step, sine, and random signals, and can be used to test the filter manually or via the verification pipeline described below.
To run the bit-exact verification against the Python reference model, execute:
make test
This will:
- Generate deterministic test vectors,
- Run the C implementation on each vector,
- Compare the output against the Python reference model output to verify bit-exactness.
Example output:
PASS (bit-exact): impulse.txt
PASS (bit-exact): step.txt
PASS (bit-exact): sine.txt
PASS (bit-exact): rand.txt
To generate the magnitude response and −3 dB bandwidth plot (as shown in the Magnitude Response section), run:
make plots
This uses the python/analyze_cic.py script to perform the analysis, in which the default output sample rate is assumed to be 200 Hz.
The measured response is obtained via a sine sweep at the input sample rate and normalized to DC, then compared against the theoretical CIC magnitude response:
|H(e^{jω})| = |sin(ω · R · M / 2) / sin(ω / 2)|^N
where ω = 2πf / fs_in.
-
Decimators are time-varying systems
Frequency response is therefore measured via sine sweep rather than FFT of a decimated impulse response.
-
Wraparound arithmetic is intentional
This implementation models fixed-width two’s-complement behavior, not idealized infinite-precision math.
-
Passband droop and compensation
The inherent sinc^N passband droop of CIC filters is not compensated here. A low-order FIR compensation filter operating at the output sample rate would be a natural extension to improve passband flatness with minimal additional cost.
This implementation is based on the CIC filter structure as described in:
Rick Lyons, A Beginner's Guide to Cascaded Integrator–Comb (CIC) Filters
The article was used as a conceptual reference for the filter structure and behavior. All code in this repository is an original implementation.
This project is licensed under the MIT License. See the LICENSE file for details.
