From 771fa587699c1baa35d391834013720f359faf43 Mon Sep 17 00:00:00 2001 From: Joppe Blondel Date: Sun, 19 Oct 2025 17:02:29 +0200 Subject: [PATCH] Added K IIR lpf filter --- project.cfg | 2 + rtl/core/lpf_iir_q15_k.v | 64 +++++++++++++++++++ .../bode_compare_Ks_lpf_iir_q15_k.py | 42 ++++++++++++ sim/tb/tb_sigmadelta.v | 8 ++- 4 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 rtl/core/lpf_iir_q15_k.v create mode 100644 scripts/filter_design/bode_compare_Ks_lpf_iir_q15_k.py diff --git a/project.cfg b/project.cfg index 2f0e66b..53bc7fc 100644 --- a/project.cfg +++ b/project.cfg @@ -23,6 +23,7 @@ files_verilog = rtl/toplevel/top_generic.v rtl/core/sigmadelta_sampler.v rtl/core/sigmadelta_rcmodel_q15.v rtl/core/mul_const.v + rtl/core/lpf_iir_q15_k.v rtl/arch/spartan-6/lvds_comparator.v rtl/arch/spartan-6/clk_gen.v files_con = boards/mimas_v1/constraints.ucf @@ -50,6 +51,7 @@ files_verilog = sim/tb/tb_nco_q15.v rtl/core/lvds_comparator.v rtl/core/sigmadelta_rcmodel_q15.v rtl/core/mul_const.v + rtl/core/lpf_iir_q15_k.v sim/overrides/sigmadelta_sampler.v sim/overrides/clk_gen.v files_other = rtl/util/conv.vh diff --git a/rtl/core/lpf_iir_q15_k.v b/rtl/core/lpf_iir_q15_k.v new file mode 100644 index 0000000..346124f --- /dev/null +++ b/rtl/core/lpf_iir_q15_k.v @@ -0,0 +1,64 @@ +`timescale 1ns/1ps + +// ============================================================================= +// Low-Pass IIR Filter (Q1.15) +// Simple first-order infinite impulse response filter, equivalent to an +// exponential moving average. Provides an adjustable smoothing factor based +// on parameter K. +// +// Implements: +// Y[n+1] = Y[n] + (X[n] - Y[n]) / 2^K +// +// This is a purely digital one-pole low-pass filter whose time constant +// approximates that of an analog RC filter, where alpha = 1 / 2^K. +// +// The larger K is, the slower the filter responds (stronger smoothing). +// The smaller K is, the faster it reacts to changes. +// +// parameters: +// -- K : filter shift factor (integer, 4..14 typical) +// cutoff frequency ≈ Fs / (2π * 2^K) +// larger K → lower cutoff +// +// inout: +// -- clk : input clock +// -- rst_n : active-low reset +// -- x_q15 : signed 16-bit Q1.15 input sample (e.g., 0..0x7FFF) +// -- y_q15 : signed 16-bit Q1.15 filtered output +// +// Notes: +// - The arithmetic right shift implements division by 2^K. +// - Internal arithmetic is Q1.15 fixed-point with saturation +// to [0, 0x7FFF] (for non-negative signals). +// - Useful for smoothing noisy ADC / sigma-delta data streams +// or modeling an RC envelope follower. +// ============================================================================= +module lpf_iir_q15_k #( + parameter integer K = 10 // try 8..12; bigger = more smoothing +)( + input wire clk, + input wire rst_n, + input wire signed [15:0] x_q15, // Q1.15 input (e.g., 0..0x7FFF) + output reg signed [15:0] y_q15 // Q1.15 output +); + wire signed [15:0] e_q15 = x_q15 - y_q15; + wire signed [15:0] delta_q15 = e_q15 >>> K; // arithmetic shift + wire signed [15:0] y_next = y_q15 + delta_q15; // clamp to [0, 0x7FFF] (handy if your signal is non-negative) + + function signed [15:0] clamp01; + input signed [15:0] v; + begin + if (v < 16'sd0) + clamp01 = 16'sd0; + else if (v > 16'sh7FFF) + clamp01 = 16'sh7FFF; + else + clamp01 = v; + end + endfunction + + always @(posedge clk or negedge rst_n) begin + if (!rst_n) y_q15 <= 16'sd0; + else y_q15 <= clamp01(y_next); + end +endmodule diff --git a/scripts/filter_design/bode_compare_Ks_lpf_iir_q15_k.py b/scripts/filter_design/bode_compare_Ks_lpf_iir_q15_k.py new file mode 100644 index 0000000..c34b51c --- /dev/null +++ b/scripts/filter_design/bode_compare_Ks_lpf_iir_q15_k.py @@ -0,0 +1,42 @@ +import numpy as np +import matplotlib.pyplot as plt + +def bode_mag_phase(Fs, K, n=4096): + """Return frequency, magnitude (dB), and phase (deg) for 1-pole IIR.""" + alpha = 2.0 ** (-K) + a = 1.0 - alpha + w = np.linspace(0, np.pi, n) + ejw = np.exp(-1j * w) + H = alpha / (1 - a * ejw) + f = (w / (2 * np.pi)) * Fs + mag_db = 20 * np.log10(np.abs(H) + 1e-20) + phase_deg = np.unwrap(np.angle(H)) * 180 / np.pi + return f, mag_db, phase_deg + +if __name__ == "__main__": + Fs = 15e6 + Ks = [10, 8, 6, 4] + + fig, (ax_mag, ax_phase) = plt.subplots(2, 1, sharex=True, figsize=(8, 6)) + + for K in Ks: + f, mag_db, phase_deg = bode_mag_phase(Fs, K, n=32768) + ax_mag.semilogx(f, mag_db, label=f"K={K}") + ax_phase.semilogx(f, phase_deg, label=f"K={K}") + + # ---- Styling ---- + f_max = 40e3 # 40 kHz + ax_mag.set_xlim(10, f_max) # start at 10 Hz for nicer log scaling + ax_mag.set_ylim(-60, 1) + ax_mag.set_title("Ideal Bode Response vs K") + ax_mag.set_ylabel("Magnitude [dB]") + ax_mag.grid(True, which='both') + ax_mag.legend() + + ax_phase.set_xlabel("Frequency [Hz]") + ax_phase.set_ylabel("Phase [deg]") + ax_phase.grid(True, which='both') + ax_phase.set_xlim(10, f_max) + + plt.tight_layout() + plt.show() \ No newline at end of file diff --git a/sim/tb/tb_sigmadelta.v b/sim/tb/tb_sigmadelta.v index c6653ad..1948a80 100644 --- a/sim/tb/tb_sigmadelta.v +++ b/sim/tb/tb_sigmadelta.v @@ -27,7 +27,6 @@ module tb_sigmadelta(); .o(sd_o) ); - wire signed [15:0] sample_q15; sigmadelta_rcmodel_q15 rc_model( .clk(clk), .resetn(resetn), @@ -35,4 +34,11 @@ module tb_sigmadelta(); .sample_q15(sample_q15) ); + wire signed [15:0] y_q15; + lpf_iir_q15_k #(8) lpf( + .clk(clk), .rst_n(resetn), + .x_q15(sample_q15), + .y_q15(y_q15) + ); + endmodule