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 PASSED

The 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 >>> F rescale and sign handling are correct.

This version adds round-half-up (add half an LSB at bit F-1 before 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
endmodule
PASS 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 PASSED

The 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 as 2*W bits.

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 W signed 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 is rnd = 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 against MAXP and MINP computed 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 Lab

Open this design in the free in-browser simulator — edit, compile and see the waveform/log instantly.

Run it in the Lab

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
Next batch starts 18th November 2026
Start your VLSI career with ChipXpert

Live online and classroom batches in Hyderabad & Bengaluru. Fill in your details and a counsellor will call you back.

  • Real EDA tools in your browser: industry-standard EDAReal EDA tools in your browser
  • Recorded sessions on the elearn portalRecorded sessions
  • Placement assistance: resume, mock interviewsPlacement assistance
  • Merit scholarship up to 60% · EMI optionsScholarship up to 60%
Hi! Ask me about courses, fees, batches or discounts. ×
BENGALURU
HYDERABAD