Fixed-Point Multiplier in Verilog: Q-Format Multiply with Code
A fixed-point multiplier multiplies two signed numbers in Qm.n format and returns a result in the same format, so the binary point stays where the rest of your datapath expects it. Multiplying two Qm.n values gives a double-width 2m.2n product; the multiplier then rescales it by an arithmetic shift right of n bits. This page gives you clean, synthesizable fixed point multiplier Verilog code with runnable testbenches, covering both plain truncation and a round-half-up variant with saturation.
Fixed-point multiply is the workhorse of DSP, digital filters, and control loops on FPGAs and ASICs, where a hardware floating-point unit would be far too expensive. Getting the rescale, the rounding, and the single overflow value right is exactly what interviewers probe, so the examples below are written to make each of those decisions explicit.
This parameterized module takes a W-bit signed operand with F fraction bits, forms the full 2*W-bit product, and truncates it back to W bits with an arithmetic shift right by F. The testbench instantiates it as both Q1.15 and Q4.12 and checks known fixed-point products against hand-computed integer codes.
module fxp_mul #(parameter W = 16, parameter F = 15) (
input signed [W-1:0] a,
input signed [W-1:0] b,
output signed [W-1:0] p
);
wire signed [2*W-1:0] full = a * b;
assign p = full >>> F;
endmodule
module tb;
reg signed [15:0] a15, b15;
wire signed [15:0] p15;
reg signed [15:0] a12, b12;
wire signed [15:0] p12;
integer errors;
fxp_mul #(.W(16), .F(15)) uq115 (.a(a15), .b(b15), .p(p15));
fxp_mul #(.W(16), .F(12)) uq412 (.a(a12), .b(b12), .p(p12));
task chk15;
input signed [15:0] av, bv, exp;
begin
a15 = av; b15 = bv; #1;
if (p15 !== exp) begin
errors = errors + 1;
$display("FAIL Q1.15 %0d*%0d => %0d exp %0d", av, bv, p15, exp);
end else
$display("PASS Q1.15 a=%0d b=%0d p=%0d", av, bv, p15);
end
endtask
task chk12;
input signed [15:0] av, bv, exp;
begin
a12 = av; b12 = bv; #1;
if (p12 !== exp) begin
errors = errors + 1;
$display("FAIL Q4.12 %0d*%0d => %0d exp %0d", av, bv, p12, exp);
end else
$display("PASS Q4.12 a=%0d b=%0d p=%0d", av, bv, p12);
end
endtask
initial begin
errors = 0;
// Q1.15: 1.0 LSB scale = 32768
chk15(16384, 16384, 8192); // 0.5 * 0.5 = 0.25
chk15(-8192, 16384, -4096); // -0.25 * 0.5 = -0.125
chk15(-16384, -16384, 8192); // -0.5 * -0.5 = 0.25
chk15(32767, 16384, 16383); // ~1.0 * 0.5 -> truncation
// Q4.12: 1.0 scale = 4096
chk12(4096, 4096, 4096); // 1.0 * 1.0 = 1.0
chk12(6144, 4096, 6144); // 1.5 * 1.0 = 1.5
chk12(6144, 6144, 9216); // 1.5 * 1.5 = 2.25
chk12(-6144, 4096, -6144); // -1.5 * 1.0 = -1.5
if (errors == 0) $display("ALL TESTS PASSED");
else $display("%0d TESTS FAILED", errors);
$finish;
end
endmodule
PASS Q1.15 a=16384 b=16384 p=8192 PASS Q1.15 a=-8192 b=16384 p=-4096 PASS Q1.15 a=-16384 b=-16384 p=8192 PASS Q1.15 a=32767 b=16384 p=16383 PASS Q4.12 a=4096 b=4096 p=4096 PASS Q4.12 a=6144 b=4096 p=6144 PASS Q4.12 a=6144 b=6144 p=9216 PASS Q4.12 a=-6144 b=4096 p=-6144 ALL TESTS PASSEDThe log shows every stimulus pair and the scaled integer result. 0.5 x 0.5 = 0.25 appears as 16384 x 16384 => 8192 in Q1.15 (16384 is 0.5, 8192 is 0.25), and 1.5 x 1.5 = 2.25 appears as 6144 x 6144 => 9216 in Q4.12. Each PASS line confirms the shifted product equals the expected code, so the
>>> Frescale and sign handling are correct.This version adds round-half-up (add half an LSB at bit
F-1before the shift) and saturates the result to the signed range so the unrepresentable +1.0 product cannot wrap to a negative number. The testbench exercises exact products, the rounding boundary, and both overflow directions.module fxp_mul_rnd_sat #(parameter W = 16, parameter F = 15) ( input signed [W-1:0] a, input signed [W-1:0] b, output reg signed [W-1:0] p ); localparam signed [2*W-1:0] MAXP = (1 <<< (W-1)) - 1; // 32767 for W=16 localparam signed [2*W-1:0] MINP = -(1 <<< (W-1)); // -32768 wire signed [2*W-1:0] full = a * b; // round half up: add half an LSB (at bit F-1) before the shift wire signed [2*W-1:0] rnd = full + (1 <<< (F-1)); wire signed [2*W-1:0] shft = rnd >>> F; always @(*) begin if (shft > MAXP) p = MAXP[W-1:0]; else if (shft < MINP) p = MINP[W-1:0]; else p = shft[W-1:0]; end endmodule module tb; reg signed [15:0] a, b; wire signed [15:0] p; integer errors; fxp_mul_rnd_sat #(.W(16), .F(15)) dut (.a(a), .b(b), .p(p)); task chk; input signed [15:0] av, bv, exp; begin a = av; b = bv; #1; if (p !== exp) begin errors = errors + 1; $display("FAIL %0d*%0d => %0d exp %0d", av, bv, p, exp); end else $display("PASS a=%0d b=%0d p=%0d", av, bv, p); end endtask initial begin errors = 0; // Q1.15, scale 32768. round-half-up adds 16384 before >>>15. // 0.5 * 0.5: full=268435456, +16384 =268451840, >>>15 =8192 (exact) chk(16384, 16384, 8192); // overflow saturation: -1.0 * -1.0 = +1.0 -> clamp to MAX 32767 chk(-32768, -32768, 32767); // another overflow attempt: 32767 * 32767 approx +1.0 -> near 32766 then +round // full=1073676289, +16384=1073692673, >>>15=32766 (<=MAX, no sat) chk(32767, 32767, 32766); // rounding demonstration: choose product with fraction exactly .5 LSB // a=3, b=16384: full=49152, +16384=65536, >>>15=2 (truncation would give 1) chk(3, 16384, 2); // a=1, b=16384: full=16384, +16384=32768, >>>15=1 (truncation gives 0) chk(1, 16384, 1); // negative no-overflow: -0.5 * 0.5 = -0.25 // full=-268435456, +16384=-268419072, >>>15 = -8191 (round half up toward +inf) chk(-16384, 16384, -8192); // min saturation path: large negative product // -32768 * 32767 = -1073709056, +16384 = -1073692672, >>>15 = -32766 (no sat, > MIN) chk(-32768, 32767, -32767); if (errors == 0) $display("ALL TESTS PASSED"); else $display("%0d TESTS FAILED", errors); $finish; end endmodulePASS a=16384 b=16384 p=8192
PASS a=-32768 b=-32768 p=32767
PASS a=32767 b=32767 p=32766
PASS a=3 b=16384 p=2
PASS a=1 b=16384 p=1
PASS a=-16384 b=16384 p=-8192
PASS a=-32768 b=32767 p=-32767
ALL TESTS PASSEDThe log proves three behaviours. Rounding: 1 x 0.5 (code 1 x 16384) yields 1 instead of the 0 truncation would give. Saturation: -1.0 x -1.0 (code -32768 x -32768) would be +1.0, which is not representable in Q1.15, so it clamps to the maximum 32767 rather than wrapping. And negative products such as -32768 x 32767 => -32767 confirm the arithmetic shift and the lower saturation bound stay correct.
Q-format and the binary point
A Qm.n number is just a
W = m + n-bit two's-complement integer with an implied binary point: the low n bits are the fraction and the rest are the integer part (the sign bit is the top integer bit). The hardware never stores a point; it only agrees that the stored integer equals the real value times 2n. So in Q1.15, the value 0.5 is stored as 0.5 x 215 = 16384, and 32767 represents the largest value just below +1.0.This convention is why fixed-point arithmetic is so cheap: addition and subtraction of two same-format numbers are plain integer add/subtract. Only multiplication needs extra care, because the scale factors multiply too.
Why the product doubles the fraction bits
Each operand carries a hidden scale of 2n. When you multiply the stored integers, you also multiply the scales: the raw product represents
(real_a x 2^n) x (real_b x 2^n) = (real_a x real_b) x 2^(2n). In other words the integer product is in Q(2m).(2n) and needs twice the width to hold it without loss, which is why the code declares the product as2*Wbits.Dropping to a narrower product before the rescale would discard the very bits that carry the fraction, so always form the full-width product first. Synthesis maps this to a single
W x Wsigned multiplier (a DSP slice on most FPGAs).The shift-back rescale
To return to Qm.n, divide the double-scaled product by 2n, which for a power of two is an arithmetic shift right by n:
p = full >>> F. The arithmetic shift (>>>on a signed value) replicates the sign bit, so negative products stay negative. A logical shift (>>) here would corrupt negative results, a classic fixed-point bug.After the shift you keep the low W bits. As long as the true result fits in Qm.n, the upper bits are just sign extension and the truncation is lossless in the integer part.
Rounding vs truncation
A plain arithmetic shift right truncates toward minus infinity: it throws away the low n bits. That introduces a small negative bias, because every result is rounded down. For filters and accumulators that bias can build up over time.
Round-half-up fixes most of it by adding half an LSB (a 1 at bit
F-1) before the shift, so a fractional part of 0.5 or more pushes the result up to the next code. In the Verilog that isrnd = full + (1 <<< (F-1)); p = rnd >>> F;. Note that because>>>still floors, exact-half negative values round toward minus infinity rather than away from zero, which the testbench verifies with the -0.5 x 0.5 case. Convergent (round-to-even) rounding removes the residual tie bias entirely but costs more logic.The single overflow value and saturation
Signed Qm.n is asymmetric: it spans -1.0 up to +1.0 minus one LSB (in Q1.15, -1.0 = -32768 but the largest positive is 32767, not +1.0). The only product that overflows is the full-scale negative times itself:
-1.0 x -1.0 = +1.0, which has no code. Left alone, the raw shift produces 32768, and truncating to 16 bits wraps it to -32768, a catastrophic sign flip.Saturation clamps the rescaled value to
[MIN, MAX]before narrowing, so that corner becomes 32767 instead of wrapping. The example compares the shifted product againstMAXPandMINPcomputed from the width, which keeps it parameterizable. Saturating arithmetic is standard in audio and DSP pipelines precisely because a single wrap can be audible or destabilizing.FAQ
Do I need the full 2W-bit product? Yes, to avoid losing fraction bits before the rescale; synthesis still infers one multiplier. Why arithmetic and not logical shift? So negative results keep their sign. When is saturation unnecessary? If your format has headroom (e.g. Q4.12 integer range) and inputs cannot reach full-scale negative simultaneously, wrap cannot occur, but saturating is cheap insurance.
Have questions about this topic?
Share your question in comments or talk to our mentor team for batch guidance.▶ Try it in the ChipXpert LabRun it in the LabOpen this design in the free in-browser simulator — edit, compile and see the waveform/log instantly.
Learn Verilog with mentors, labs and placement support
This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.
Explore VLSI Courses Hi! Ask me about courses, fees, batches or discounts. ×ChipXpert AssistantAsk about courses, fees, batches and centres