Skip to content

Ray-Research-Group/ArduinoFilters

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Arduino Filters

Lightweight DSP filters for Arduino and other embedded MCUs.

Includes a Direct Form II IIR filter, a FIR filter, and a DC-removal block — all using power-of-two circular buffers to replace expensive modulo operations with single-cycle bitwise AND. Coefficients are designed with the companion Python scripts and pasted directly into your sketch.

Developed for the Ray Research Lab


Repository layout

DanFilters/
├── src/
│   ├── DSP_Modules.h        ← template IIRFilter<SIZE>, FIRFilter<TAPS>, DCRemove
│   └── DSP_Modules.cpp      ← DCRemove implementation
├── examples/
│   ├── bandpass_iir/        ← cascade LP + HP to form a bandpass (the main demo)
│   ├── lowpass_iir/         ← single IIR low-pass, prints raw + filtered over Serial
│   └── fir_lowpass/         ← 16-tap FIR low-pass with linear phase
├── python/
│   ├── requirements.txt
│   ├── design_iir_filter.py ← design Butterworth / Chebyshev / Elliptic IIR
│   ├── design_fir_filter.py ← design windowed-sinc FIR
│   └── visualize_filter.py  ← plot frequency response, phase, group delay, spectrogram
└── library.properties

Quick start (Arduino IDE)

  1. Copy the DanFilters/ folder into your Arduino libraries/ directory, or copy src/DSP_Modules.h and src/DSP_Modules.cpp directly into your sketch folder.
  2. Include the header:
    #include <DSP_Modules.h>
  3. Paste coefficients generated by the Python scripts (see below).
  4. Instantiate a filter and call process() once per sample.

PlatformIO

Add this to platformio.ini:

lib_deps =
    file://path/to/DanFilters

How to use IIRFilter

#include <DSP_Modules.h>

// Coefficients from design_iir_filter.py (Butterworth low-pass, 100 Hz, 1 kHz fs)
const float b[3] = {0.00024135904904198073f, 0.00048271809808396145f, 0.00024135904904198073f};
const float a[3] = {1.0f, -1.9555782403150352f, 0.9565436765112031f};

// Template parameter must be a power of two >= (max(len(b), len(a)) - 1)
// For a 2nd-order filter (3 coefficients) use SIZE=4.
IIRFilter<4> lpf(b, 3, a, 3);

void loop() {
    float raw      = analogRead(A0);
    float filtered = lpf.process(raw);
    Serial.println(filtered);
}

Cascading filters (bandpass)

IIRFilter<4> lpf(b_lp, 3, a_lp, 3);  // low-pass at upper edge
IIRFilter<4> hpf(b_hp, 3, a_hp, 3);  // high-pass at lower edge

float y = hpf.process(lpf.process(analogRead(A0)));

Choosing SIZE

Filter order Coefficients Minimum SIZE
1st 2 2
2nd 3 4
3rd 4 4
4th 5 8
8th 9 16

Always round up to the next power of two.


How to use FIRFilter

#include <DSP_Modules.h>

// 16-tap FIR (from design_fir_filter.py). TAPS must be a power of two.
// Zero-pad if your design has a non-power-of-two tap count.
const float h[16] = {
    0.00338635f, 0.00978454f, 0.02194054f, 0.04027368f,
    0.06273524f, 0.08612576f, 0.10567416f, 0.11754896f,
    0.11754896f, 0.10567416f, 0.08612576f, 0.06273524f,
    0.04027368f, 0.02194054f, 0.00978454f, 0.00338635f
};

FIRFilter<16> fir(h);

void loop() {
    float y = fir.process(analogRead(A0));
    Serial.println(y);
}

FIR vs IIR: FIR filters always have linear phase (constant group delay), which matters for applications like ECG or audio where phase distortion is audible/visible. IIR filters achieve the same roll-off with far fewer coefficients but introduce non-linear phase.


How to use DCRemove

DCRemove dc(0.995f);  // R close to 1.0 = very low corner frequency

void loop() {
    float y = dc.process(analogRead(A0));  // removes DC bias
}

The transfer function is H(z) = (1 - z⁻¹) / (1 - R·z⁻¹).

  • R = 0.9 → corner ~16 Hz at 1 kHz
  • R = 0.995 → corner ~0.8 Hz at 1 kHz
  • R = 0.999 → corner ~0.16 Hz at 1 kHz

Python design tools

Setup

cd python
pip install -r requirements.txt

Design an IIR filter

# 2nd-order Butterworth low-pass at 100 Hz, 1 kHz sample rate
python design_iir_filter.py --type lowpass --fs 1000 --cutoff 100 --order 2

# 2nd-order Butterworth band-pass 5-100 Hz (equivalent to LP→HP cascade)
python design_iir_filter.py --type bandpass --fs 1000 --low 5 --high 100 --order 2

# 3rd-order Chebyshev Type I high-pass, 50 Hz, 1 dB passband ripple
python design_iir_filter.py --type highpass --fs 1000 --cutoff 50 --order 3 \
    --family cheby1 --rp 1

# Add --plot to show the frequency and phase response
python design_iir_filter.py --type lowpass --fs 1000 --cutoff 100 --order 4 --plot

Output (paste directly into your sketch):

// ── IIR Filter Coefficients (auto-generated) ──────────────
// Type   : lowpass
// Family : butter
// Order  : 2
// fs     : 1000 Hz
// Cutoff : 100 Hz
// ──────────────────────────────────────────────────────────

const float b[3] = {0.00024135904904198073f, 0.00048271809808396145f, 0.00024135904904198073f};
const float a[3] = {1.0f, -1.9555782403150352f, 0.9565436765112031f};

const int nB = 3;
const int nA = 3;

IIRFilter<4> filter(b, nB, a, nA);

Design a FIR filter

# 32-tap Hamming-windowed low-pass at 100 Hz
python design_fir_filter.py --fs 1000 --cutoff 100 --taps 32

# 64-tap band-pass 5-100 Hz
python design_fir_filter.py --fs 1000 --low 5 --high 100 --taps 64

# Kaiser window, beta=8 (high stopband attenuation)
python design_fir_filter.py --fs 1000 --cutoff 100 --taps 32 --window kaiser --beta 8 --plot

Visualize a filter

# Plot IIR response (edit b/a in the script or pipe from design_iir_filter.py --plot)
python visualize_filter.py --mode iir --fs 1000

# Plot time-domain raw vs filtered from a Serial CSV log
python visualize_filter.py --mode time --fs 1000 --csv data.csv

# Spectrogram of filtered output
python visualize_filter.py --mode spectrogram --fs 1000 --csv data.csv

To capture Serial data as CSV on Linux/macOS:

# Replace /dev/tty.usbmodem* with your port
cat /dev/tty.usbmodem* > data.csv

Algorithm details

Direct Form II IIR

For a 2nd-order section the update equations are:

w[n] = x[n] − a₁·w[n−1] − a₂·w[n−2]   (feedback)
y[n] = b₀·w[n] + b₁·w[n−1] + b₂·w[n−2]  (feedforward)

Direct Form II uses only one delay line (w) instead of two (x and y in Direct Form I), halving the state storage for the same transfer function.

Power-of-two circular buffer

The state array wHist[SIZE] acts as a circular buffer. Instead of:

writeIndex = (writeIndex + 1) % SIZE;  // integer division — slow on AVR

the library uses:

writeIndex = (writeIndex + 1) & mask;  // mask = SIZE - 1, single AND instruction

This is valid only when SIZE is a power of two, enforced by a static_assert at compile time.

DC blocker

y[n] = x[n] − x[n−1] + R · y[n−1]

A first-order IIR high-pass at near-DC frequency. It removes any constant offset (battery drift, sensor bias) before the main filter sees the signal.


License

MIT

About

Lightweight DSP filters for Arduino and other embedded MCUs.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages

  • Jupyter Notebook 98.5%
  • Python 1.1%
  • Other 0.4%