ALU - PROGRAMMABLE FIR FILTER
Runtime-configurable FIR (Finite Impulse Response) filter with programmable coefficients. Supports two loading modes: PARALLEL (N coefficient ports) or SERIAL (address + data interface). Double-buffered coefficients enable glitch-free updates via LOAD signal.
Introduction
The Programmable FIR Filter block implements a runtime-configurable finite impulse response filter:
$$ y[n] = \sum_{k=0}^{N-1} h[k] \cdot x[n-k] $$
Unlike the static FIR Filter, coefficients can be changed during operation without recompiling the design. This enables:
- Adaptive filtering algorithms
- Runtime filter tuning
- Multiple filter responses with a single block
Two coefficient loading modes are supported:
- PARALLEL: N separate coefficient input ports (coef_0, coef_1, …, coef_N-1)
- SERIAL: Address + data + write enable interface for sequential loading
Pin Description
COEF_0 … COEF_N-1 bus; the
remaining ports behave identically and are not repeated here.
Width: Coefficient Data Bits.
COEF_0 carries h[0], COEF_1 carries h[1], and so on.
Values are continuously sampled into the shadow registers and only become
active on the LOAD rising edge.
Properties
How coefficients are loaded: PARALLEL (N ports) or SERIAL (address + data)
Coefficient loading interface mode:
- Parallel: N individual coefficient ports (coef_0 to coef_N-1)
- Serial: Address + data + write enable interface
Choose PARALLEL for fast updates with few taps. Choose SERIAL for many taps or limited I/O resources.
Default: PARALLEL
Options: PARALLEL SERIAL
Filter order (number of coefficients, 2-64)
Number of filter taps (coefficients). Range: 2-64. Determines the filter order (order = NumTaps - 1). Higher values allow sharper frequency transitions but use more resources.Default: 8
Options: 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
Total bit width of input data (8-32 bits)
Total bit width of input data X. Range: 8-32 bits. Includes both integer and fractional parts.Default: 16
Options: 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
Number of fractional bits in input data (0-16)
Number of fractional bits in input data. Range: 0-16 bits. Integer bits = InDataBits - InFractBits.Default: 0
Options: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
Total bit width of output data (8-64 bits)
Total bit width of output data Y. Range: 8-64 bits. Should be large enough to avoid overflow. Recommended: InDataBits + CoefDataBits + ceil(log2(NumTaps)).Default: 32
Options: 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
Number of fractional bits in output data (0-32)
Number of fractional bits in output data. Range: 0-32 bits. Should match: InFractBits + CoefFractBits for no precision loss.Default: 16
Options: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
Total bit width of coefficients (8-32 bits)
Total bit width of filter coefficients. Range: 8-32 bits. Higher values improve coefficient precision.Default: 16
Options: 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
Number of fractional bits in coefficients (0-31)
Number of fractional bits in coefficients. Range: 0-31 bits. For normalized coefficients (|h| < 1), use CoefFractBits = CoefDataBits - 1.Default: 15
Options: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
Functional description
The filter implements discrete convolution with runtime-programmable coefficients:
$$ y[n] = h_0 \cdot x[n] + h_1 \cdot x[n-1] + h_2 \cdot x[n-2] + \ldots + h_{N-1} \cdot x[n-N+1] $$
Double-buffered coefficient loading
Coefficients are double-buffered to ensure glitch-free updates:
- Shadow registers: New coefficient values are written to shadow registers
- Active registers: Currently used for filtering
- LOAD signal: On rising edge, shadow registers are copied to active registers atomically
This architecture ensures:
- No partial coefficient updates during filtering
- Deterministic switching behavior
- No output glitches during coefficient changes
Loading modes
PARALLEL mode
All coefficients are presented simultaneously on dedicated input ports:
| Port | Description |
|---|---|
| coef_0 | Coefficient h[0] |
| coef_1 | Coefficient h[1] |
| … | … |
| coef_N-1 | Coefficient h[N-1] |
| LOAD | Rising edge transfers all coefficients |
Advantages: Single-cycle coefficient update, simple control Disadvantages: Many input ports for large N
SERIAL mode
Coefficients are loaded sequentially through an address/data interface:
| Port | Description |
|---|---|
| COEF_DATA | Coefficient value to write |
| COEF_ADDR | Target coefficient index (0 to N-1) |
| COEF_WR | Write enable (active high) |
| LOAD | Rising edge transfers all coefficients |
Loading sequence:
- Set COEF_ADDR to target index
- Set COEF_DATA to coefficient value
- Pulse COEF_WR high for one clock cycle
- Repeat for all coefficients
- Pulse LOAD to activate new coefficients
Advantages: Only 4 control ports regardless of N Disadvantages: N clock cycles to load all coefficients
Transfer function
In the z-domain:
$$ H(z) = \sum_{k=0}^{N-1} h[k] \cdot z^{-k} $$
The transfer function changes when new coefficients are loaded.
Fixed-point format
All data paths use configurable fixed-point arithmetic:
| Parameter | Format | Description |
|---|---|---|
| Input | Q(I-F).F | I = InDataBits, F = InFractBits |
| Output | Q(O-F).F | O = OutDataBits, F = OutFractBits |
| Coefficients | Q(C-F).F | C = CoefDataBits, F = CoefFractBits |
The output uses rounding (AP_RND) and saturation (AP_SAT) to prevent overflow.
Typical applications
- Adaptive filtering: LMS, RLS, or other adaptive algorithms
- Equalizers: Channel equalization in communications
- Runtime tuning: User-adjustable filter response
- Multi-mode filters: Switch between different filter responses
- Calibration: Compensate for system variations
Latency
Fixed latency of 4 clock cycles (HLS pipeline) plus delay line fill time. The DV output goes high after N samples have been processed.
Coefficient updates take effect on the next LOAD rising edge. In SERIAL mode, allow N+1 clock cycles for full coefficient loading before LOAD.
Python examples for runtime coefficient generation
These examples show how to generate and update coefficients at runtime.
Basic coefficient generation
python
import numpy as np
from scipy import signal
def generate_lowpass(num_taps, cutoff, coef_fract_bits=15):
"""Generate low-pass filter coefficients."""
coefficients = signal.firwin(num_taps, cutoff, window='hamming')
scale = 2 ** coef_fract_bits
quantized = np.round(coefficients * scale).astype(int)
return quantized
# Generate 16-tap low-pass with cutoff at 0.2 * Nyquist
coefs = generate_lowpass(16, 0.2)
print("Low-pass coefficients:", list(coefs))
Adaptive filter example (LMS algorithm)
python
import numpy as np
class LMSFilter:
"""Least Mean Squares adaptive filter for hardware implementation."""
def __init__(self, num_taps, mu=0.01, coef_fract_bits=15):
self.num_taps = num_taps
self.mu = mu # Step size
self.coef_fract_bits = coef_fract_bits
self.scale = 2 ** coef_fract_bits
self.weights = np.zeros(num_taps)
self.buffer = np.zeros(num_taps)
def update(self, x, d):
"""
Update filter with new sample.
x: input sample
d: desired output
Returns: (output, error, quantized_weights)
"""
# Shift buffer
self.buffer = np.roll(self.buffer, 1)
self.buffer[0] = x
# Compute output
y = np.dot(self.weights, self.buffer)
# Compute error
e = d - y
# Update weights (LMS)
self.weights += 2 * self.mu * e * self.buffer
# Quantize weights for hardware
quantized = np.round(self.weights * self.scale).astype(int)
return y, e, quantized
def get_coefficients(self):
"""Get current coefficients in hardware format."""
return np.round(self.weights * self.scale).astype(int)
# Example usage
lms = LMSFilter(num_taps=16, mu=0.01)
# Simulate adaptation (in practice, run on actual signals)
for i in range(1000):
x = np.random.randn() # Input
d = x * 0.5 # Desired output (example: gain of 0.5)
y, e, coefs = lms.update(x, d)
print("Adapted coefficients:", list(lms.get_coefficients()))
Coefficient streaming for SERIAL mode
python
import numpy as np
def generate_serial_sequence(coefficients, coef_bits=16):
"""
Generate sequence of (addr, data, wr) tuples for SERIAL mode loading.
"""
sequence = []
for addr, coef in enumerate(coefficients):
# Ensure coefficient fits in specified bits (signed)
max_val = 2 ** (coef_bits - 1) - 1
min_val = -2 ** (coef_bits - 1)
coef_clamped = max(min_val, min(max_val, int(coef)))
sequence.append({
'addr': addr,
'data': coef_clamped,
'wr': 1
})
# Add LOAD pulse at end
sequence.append({
'addr': 0,
'data': 0,
'wr': 0,
'load': 1
})
return sequence
# Example: Generate loading sequence
from scipy import signal
coefs = signal.firwin(16, 0.3) * (2**15)
coefs = coefs.astype(int)
sequence = generate_serial_sequence(coefs)
for step in sequence:
print(step)
Multi-mode filter bank
python
import numpy as np
from scipy import signal
class FilterBank:
"""Pre-computed filter bank for runtime switching."""
def __init__(self, num_taps, coef_fract_bits=15):
self.num_taps = num_taps
self.scale = 2 ** coef_fract_bits
self.filters = {}
def add_lowpass(self, name, cutoff):
"""Add low-pass filter to bank."""
coefs = signal.firwin(self.num_taps, cutoff, window='hamming')
self.filters[name] = np.round(coefs * self.scale).astype(int)
def add_highpass(self, name, cutoff):
"""Add high-pass filter to bank."""
coefs = signal.firwin(self.num_taps, cutoff, window='hamming', pass_zero=False)
self.filters[name] = np.round(coefs * self.scale).astype(int)
def add_bandpass(self, name, low, high):
"""Add band-pass filter to bank."""
coefs = signal.firwin(self.num_taps, [low, high], window='hamming', pass_zero=False)
self.filters[name] = np.round(coefs * self.scale).astype(int)
def add_custom(self, name, coefficients):
"""Add custom filter coefficients."""
self.filters[name] = np.round(np.array(coefficients) * self.scale).astype(int)
def get(self, name):
"""Get filter coefficients by name."""
return self.filters.get(name)
def list_filters(self):
"""List available filters."""
return list(self.filters.keys())
# Create filter bank
bank = FilterBank(num_taps=32)
bank.add_lowpass('lp_narrow', 0.1)
bank.add_lowpass('lp_wide', 0.4)
bank.add_highpass('hp_dc_block', 0.05)
bank.add_bandpass('bp_mid', 0.2, 0.4)
# Get coefficients for hardware
print("Available filters:", bank.list_filters())
print("LP narrow:", list(bank.get('lp_narrow')))
Real-time equalizer
python
import numpy as np
from scipy import signal
def generate_equalizer(num_taps, bands, gains, fs=1.0, coef_fract_bits=15):
"""
Generate equalizer coefficients.
bands: List of (low, high) frequency pairs (normalized 0-1)
gains: List of gains for each band (linear, not dB)
"""
# Create frequency/gain specification
freq = [0]
gain = [gains[0] if bands[0][0] == 0 else 0]
for (low, high), g in zip(bands, gains):
freq.extend([low, high])
gain.extend([g, g])
freq.append(1)
gain.append(gains[-1] if bands[-1][1] == 1 else 0)
# Design filter
coefs = signal.firwin2(num_taps, freq, gain)
scale = 2 ** coef_fract_bits
return np.round(coefs * scale).astype(int)
# Example: 3-band equalizer
# Bass boost, mid cut, treble boost
bands = [(0, 0.15), (0.15, 0.5), (0.5, 1.0)]
gains = [1.5, 0.7, 1.3] # Linear gains
eq_coefs = generate_equalizer(32, bands, gains)
print("Equalizer coefficients:", list(eq_coefs))
Coefficient update timing analysis
python
def estimate_update_time(num_taps, mode, clock_freq_mhz):
"""
Estimate time to update all coefficients.
mode: 'parallel' or 'serial'
clock_freq_mhz: System clock frequency in MHz
"""
clock_period_ns = 1000 / clock_freq_mhz
if mode == 'parallel':
# Single clock cycle for all coefficients
cycles = 1
else:
# N cycles to write + 1 for LOAD
cycles = num_taps + 1
time_ns = cycles * clock_period_ns
time_us = time_ns / 1000
print(f"Mode: {mode}")
print(f"Clock: {clock_freq_mhz} MHz ({clock_period_ns:.2f} ns period)")
print(f"Taps: {num_taps}")
print(f"Update cycles: {cycles}")
print(f"Update time: {time_ns:.2f} ns ({time_us:.3f} us)")
return cycles, time_ns
# Compare modes for 32-tap filter at 100 MHz
estimate_update_time(32, 'parallel', 100)
print()
estimate_update_time(32, 'serial', 100)