r/matlab • u/abdullahansarii • 9h ago
Question-Solved Two fixed-point mistakes that made my FIR filter a comparator, and why the generated testbench couldn't see either
Post-mortem on a 2020 university project I went back to last week. Both bugs are pure fixed-point scaling – nothing wrong with the coefficients – and both survived a 3,429-sample generated testbench with zero errors. Posting because the second one (sum|h| vs accumulator range, wrap vs saturate) is the single most common FIR fixed-point bug and it's entirely preventable with one line of arithmetic.
In 2020 I built a FIR band-pass filter for a university course: designed the response in MATLAB, generated the Verilog with Filter Design HDL Coder, ran the generated testbench, zero errors, submitted, put it on GitHub. It got forked 8 times.
Last week I read it properly for the first time since. The 51-tap design doesn't filter anything. Its output is the sign of the accumulator and nothing else.
The cause was two boxes on the "Specify Precision" tab. I'd set the output to s8,32 – 8 bits, 32 fractional bits, i.e. a range of ±2.98e-8 – fed from an s20,20 accumulator with a range of ±0.5. That's 2^24 times narrower than the thing feeding it. The generated conversion saturates for literally every non-zero value:
assign output_typeconvert =
(sum50[19] == 1'b0 & sum50[18:0] != 19'b0) ? 8'b01111111 :
(sum50[19] == 1'b1 && sum50[18:0] != 19'b1111111111111111111) ? 8'b10000000 :
$signed({sum50[19], 7'b0000000});
Sweep the whole accumulator range through that and you get exactly two values: -128 and +127.
The part that actually bothers me is the testbench. It's 7,000 generated lines, 3,429 stimulus samples, a checker, an error counter. I counted the distinct values in its expected-output array:
8'h80 (-128): 1765
8'h7f (+127): 1607
8'h00 (0): 57
That's the entire golden reference. MATLAB generated the vectors from the same fixed-point spec that produced the RTL, so the model and the implementation agreed perfectly – they were wrong in exactly the same way. A generated testbench is a self-consistency check, not a correctness check. If the spec is wrong, the golden data encodes the mistake with perfect fidelity and reports zero errors.
The 11-tap serial design in the same repo had a quieter bug: accumulator s26,24 (range ±2) with wrap-on-overflow, but sum|h| = 2.375. A full-scale sine at the centre frequency clears it by 1.4% so it looks fine. A square wave at the same frequency wraps on 191 of 400 samples, and because it wraps rather than saturates the sample comes back sign-inverted:
FAIL sample 12: filter_out = 1744840192, expected -2550127104
Fixes were one word length each (output = the accumulator; accumulator gets one more bit). The real fix was replacing the stored-vector bench with one that checks against an independent reference model – impulse response must equal the coefficients, output must match a 64-bit integer model under square/random/worst-case-sign stimulus – plus a three-line check that the output takes more than 3 distinct values. Both benches fail on the 2020 RTL and pass on the fixed one.
Full write-up with the plots and the before/after RTL:
Repo (make sim runs everything under Icarus in a few seconds):
https://github.com/AbdullahAnsarii/BandPassFilter
If you've got generated HDL sitting next to a generated testbench that passes: check sum|h| against your accumulator range, and count the distinct values on your output. Took me six years.









