Lab 5.2 — FIR RTL Mapping¶
Goal¶
Map the fixed-point FIR from Block 4 into an RTL architecture and define its ports, latency, accumulator width and testbench strategy.
This lab is the first step from algorithmic FIR filtering to a synthesizable streaming FPGA block.
Executable HDL package¶
| File | Purpose |
|---|---|
blocks/block_05_fpga_hdl_flow/rtl/fir_iq_4tap.v |
executable educational 4-tap IQ FIR RTL block |
blocks/block_05_fpga_hdl_flow/tb/tb_fir_iq_4tap.v |
self-checking Verilog testbench |
blocks/block_05_fpga_hdl_flow/python/generate_fir_iq_4tap_vectors.py |
deterministic reference-vector generator |
blocks/block_05_fpga_hdl_flow/tb/fir_iq_4tap_input_vectors.txt |
generated input vectors |
blocks/block_05_fpga_hdl_flow/tb/fir_iq_4tap_expected_vectors.txt |
generated expected output vectors |
Run from the repository root:
python blocks/block_05_fpga_hdl_flow/python/generate_fir_iq_4tap_vectors.py
iverilog -g2012 \
-o blocks/block_05_fpga_hdl_flow/tb/tb_fir_iq_4tap.out \
blocks/block_05_fpga_hdl_flow/rtl/fir_iq_4tap.v \
blocks/block_05_fpga_hdl_flow/tb/tb_fir_iq_4tap.v
vvp blocks/block_05_fpga_hdl_flow/tb/tb_fir_iq_4tap.out
Expected result:
PASS: fir_iq_4tap test completed without errors
The GitHub Actions workflow .github/workflows/block5_hdl.yml generates vectors and runs this simulation automatically.
Engineering question¶
How does a Q1.15 FIR model become a clocked RTL datapath with explicit multipliers, accumulator growth, rounding and saturation?
Inputs from Block 4¶
| Item | Example value | RTL consequence |
|---|---|---|
| Input format | Q1.15 | signed 16-bit input ports |
| Coefficient format | Q1.15 | signed 16-bit coefficient ROM |
| Number of taps | 4 in executable lab, 129 in full design | shift register length and multiplier count |
| Product format | Q2.30 | 32-bit products |
| Guard bits | ceil(log2(N)) | accumulator must be wider than product |
| Output format | Q1.15 | rounding/saturation before output |
FIR datapath¶
flowchart LR
IN[Input sample] --> SR[Shift register]
SR --> MUL[Tap multipliers]
COEF[Coefficient ROM] --> MUL
MUL --> SUM[Adder tree / accumulator]
SUM --> ROUND[Round to Q1.15]
ROUND --> SAT[Saturate]
SAT --> OUT[Output sample]
Direct-form FIR equation¶
y[n] = sum_{k=0}^{N-1} h[k] * x[n-k]
For complex IQ, the same real coefficient FIR is applied independently to I and Q:
y_i[n] = sum h[k] * x_i[n-k]
y_q[n] = sum h[k] * x_q[n-k]
Executable 4-tap example¶
The executable lab uses this Q1.15 coefficient set:
h = [0.125, 0.375, 0.375, 0.125]
h_q15 = [4096, 12288, 12288, 4096]
It is intentionally small enough to understand in a waveform viewer, but it still demonstrates the complete fixed-point FIR pattern:
- shift register;
- coefficient multiplication;
- accumulator growth;
- rounding back to Q1.15;
- saturation;
- output valid alignment;
- reference-vector comparison.
Architecture options¶
| Architecture | DSP usage | Throughput | Latency | When to use |
|---|---|---|---|---|
| Fully parallel | high | one sample/clock | low/medium | high-rate streaming |
| Time-multiplexed MAC | low | one sample per many clocks | high | low-rate or resource-limited design |
| Symmetric FIR | medium | one sample/clock | medium | linear-phase symmetric coefficients |
| Polyphase FIR | medium/high | efficient rate change | medium | decimator/interpolator |
Accumulator sizing¶
For signed Q1.15 input and Q1.15 coefficients:
product width = 16 + 16 = 32 bits
product fractional bits = 15 + 15 = 30 bits
guard bits = ceil(log2(Ntaps))
accumulator width >= 32 + guard_bits
For the executable 4-tap lab:
guard_bits = 2
accumulator width >= 34 bits
For a 129-tap practical FIR:
guard_bits = 8
accumulator width >= 40 bits
RTL skeleton¶
module fir_iq_stream #(
parameter integer W = 16,
parameter integer NTAPS = 129,
parameter integer ACC_W = 40
)(
input wire clk,
input wire rst,
input wire in_valid,
input wire signed [W-1:0] in_i,
input wire signed [W-1:0] in_q,
output reg out_valid,
output reg signed [W-1:0] out_i,
output reg signed [W-1:0] out_q
);
// Educational skeleton: actual coefficient ROM, shift register,
// multiplier array, adder tree, rounding and saturation are added
// step-by-step in implementation labs.
endmodule
Testbench strategy¶
Use reference vectors generated by Python/MATLAB Lab 4.1 or by the local FIR vector generator.
Recommended tests:
- impulse input -> output equals coefficient sequence;
- constant input -> output approaches DC gain;
- two-tone input -> interferer suppression matches reference;
- random IQ vector -> sample-by-sample comparison after latency compensation;
- saturation stress vector -> output clips deterministically.
Latency documentation¶
Latency must be stated explicitly:
| Stage | Latency, clocks |
|---|---|
| Input register | 1 |
| Shift register update | 0/1 |
| Multiplier pipeline | 1–3 |
| Adder tree | depends on tree depth |
| Rounding/saturation | 1 |
| Output register | 1 |
Vivado resource report template¶
| Resource | Estimated | Synthesized | Comment |
|---|---|---|---|
| LUT | control + adders | ||
| FF | registers + valid pipeline | ||
| DSP | multipliers | ||
| BRAM | coefficient storage if used | ||
| Latency | clocks | ||
| Fmax | MHz |
Reference result for this RTL, Vivado 2021.1 out-of-context synthesis on the course part xc7z020clg400-2 with a 100 MHz clock (python tools/generate_block5_vivado_reports.py, raw reports in reports/fpga/vivado_ooc_raw):
| Resource | Synthesized | Comment |
|---|---|---|
| LUT | 117 | adders, rounding and saturation |
| FF | 129 | shift register, outputs, valid |
| DSP48E1 | 4 | constant multiplications mapped to DSP slices |
| BRAM | 0 | coefficients are constants |
| Latency | 1 clock | from in_valid to out_valid |
| Timing at 100 MHz | WNS -0.125 ns | misses by a narrow margin: 20 logic levels, estimate about 98.8 MHz (synthesis only) |
| Timing after placement and routing | WNS -0.247 ns | still a miss, same 20-level path; port paths timed (report) |
What to expect¶
PASS: fir_iq_4tap test completed without errors
The comparison is bit-exact: the Python generator computes the same h = [0.125, 0.375, 0.375, 0.125] filter in integers with round-half-up and saturation, and every output sample must match exactly. The DC gain is 1.0, so a constant input comes out unchanged once the four taps are filled.
The course runner generates the vectors, compiles, simulates and turns any FAIL line or non-zero simulator exit into an error:
python tools/run_block5_hdl_smoke.py --test tb_fir_iq_4tap
Exercises¶
Each exercise below is a deliberate one-line RTL mutation. Make it, run the bench, read the messages, then restore the file (git checkout -- <file>). The quoted outputs were observed with Icarus Verilog 12.0.
- Replace rounding with truncation:
round_q15 = value >>> SHIFT;. The bench reports 6 errors, each exactly 1 LSB low, for exampleout=(4095,0) expected=(4096,0)andout=(12287,4095) expected=(12288,4096). Truncation is a -0.5 LSB bias; a floating-point comparison with a 1e-3 tolerance would never see it, a bit-exact bench does. - Change
H1from16'sd12288to16'sd12289(one coefficient LSB). The bench reports only 4 errors, for exampleout=(12289,4096) expected=(12288,4096). The other outputs are unchanged because the extrax * 2^-15did not cross a rounding boundary. What does that say about how many vectors a coefficient check needs? - The coefficients sum to exactly 1.0. Compute the worst-case accumulator width for 16-bit inputs and justify (or shrink)
ACC_W = 40. - The critical path at 100 MHz is not where it looks. Put a register after the multipliers (latency 2) and synthesize again: WNS stays at -0.125 ns. The timing report shows why: the path now starts at the product registers and still runs through the 40-bit sum, the rounding adder and the saturation compare in one clock (20 levels, 15 of them CARRY4). Register the sum as well and do rounding and saturation in a third clock: WNS becomes +5.35 ns with 12 logic levels (70 LUT, 67 FF, 6 DSP48E1). Both variants were checked bit-exact against this RTL on 2000 random samples, just 1 and 2 clocks later. Which change would you keep in a real design, and what does it cost in latency?
Answers (checked against the RTL and the bench)
- The observed output is in the exercise: a bit-exact bench sees a 1-LSB bias that any tolerance-based comparison would accept.
- A one-LSB coefficient change only shows where the products sit near a rounding boundary, here 4 outputs. A coefficient check needs many vectors spread over the input range, including values that make the products land near half an LSB; a handful of "nice" inputs can miss a wrong coefficient entirely.
- All four coefficients are positive and sum to
2^15(1.0 in Q1.15), so|Σ x·h| ≤ 2^15 · 2^15 = 2^30: 31 magnitude bits plus a sign, 32 bits.ACC_W = 40leaves 8 unused guard bits for this filter; 32 is enough. For a general 4-tap filter withΣ|h|up to4 · 2^15you would need 34 bits. - Keep the third stage: rounding and saturation in their own clock take the path from 20 to 12 logic levels and WNS from −0.125 to +5.35 ns for 2 extra clocks of latency and a few flip-flops. A filter feeding a stream almost never minds two clocks; a feedback loop around it (a PLL, an AGC) would, and that is where you would look for a different split.
Report checklist¶
- State FIR format and number of taps.
- Compute product and accumulator widths.
- Select architecture: parallel, time-multiplexed, symmetric or polyphase.
- Draw datapath diagram.
- Define streaming ports.
- Define latency.
- Define test vectors.
- Define pass/fail error tolerance.
- Add resource estimate table.
Engineering conclusion template¶
The FIR RTL mapping uses ____ taps with Q1.15 input and coefficients.
The product width is ____ bits and the accumulator width is ____ bits.
The selected architecture is ____ because ______.
The expected latency is ____ clocks and the main FPGA cost is ______.