Xilinx
Block Preview

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

X Input Variable bit BIT VECTOR
Input data stream to be filtered. Width: Input Data Bits. Fixed-point format: Q(InDataBits-InFractBits).InFractBits. Signed (two’s complement).
Default: Must be connected
COEF_0 Input Variable bit BIT VECTOR
(PARALLEL mode only) – first of the 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.
COEF_1 Input 16 bit BIT VECTOR
COEF_2 Input 16 bit BIT VECTOR
COEF_3 Input 16 bit BIT VECTOR
COEF_4 Input 16 bit BIT VECTOR
COEF_5 Input 16 bit BIT VECTOR
COEF_6 Input 16 bit BIT VECTOR
COEF_7 Input 16 bit BIT VECTOR
LOAD Input 1 bit BIT
Coefficient load trigger. On rising edge, shadow coefficients are transferred to active registers. In PARALLEL mode: All coef_N ports are sampled. In SERIAL mode: Previously written shadow values become active.
Default: Must be connected
CLK Input 1 bit BIT
System clock. Rising edge triggers filter computation and coefficient loading.
Default: Default Board Clock
RESET Input 1 bit BIT
Synchronous reset, active high. Clears delay line and resets warmup counter. Does NOT clear coefficients.
Default: Default Board Reset
Y Output Variable bit BIT VECTOR
Filtered output signal. Width: Output Data Bits. Fixed-point format: Q(OutDataBits-OutFractBits).OutFractBits. Includes rounding and saturation.
DV Output 1 bit BIT
Data valid output, active high. Goes high after the delay line is filled (N samples processed). Indicates Y contains valid filtered data.
COEF_DATA Variable bit
(SERIAL mode only) Coefficient data input. Width: Coefficient Data Bits. Value to write to the coefficient at address COEF_ADDR.
COEF_ADDR Variable bit
(SERIAL mode only) Coefficient address input. Width: ceil(log2(NumTaps)). Specifies which coefficient (0 to N-1) to write.
COEF_WR 1 bit
(SERIAL mode only) Coefficient write enable, active high. When high, COEF_DATA is written to shadow register at COEF_ADDR.

Properties

Property window

Coefficient Load Mode LoadMode

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

Number of Taps NumTaps

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

Input Data Bits InDataBits

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

Input Fractional Bits InFractBits

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

Output Data Bits OutDataBits

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

Output Fractional Bits OutFractBits

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

Coefficient Data Bits CoefDataBits

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

Coefficient Fractional Bits CoefFractBits

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:

  1. Shadow registers: New coefficient values are written to shadow registers
  2. Active registers: Currently used for filtering
  3. 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:

  1. Set COEF_ADDR to target index
  2. Set COEF_DATA to coefficient value
  3. Pulse COEF_WR high for one clock cycle
  4. Repeat for all coefficients
  5. 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)