Module 7: Advanced RTL Design Techniques ยท 7.2 Arithmetic Circuits in RTL

Pipelining a Multiplier

In the previous article we split an adder. If an adder’s carry chain is sometimes long enough to need pipelining, a multiplier’s critical path is long enough that it usually does, at any width worth calling wide.

Recall Module 2: an N by N array multiplier’s critical path is $O(N)$ adder stages, and even a Wallace tree only brings the reduction down to $O(\log N)$ levels of carry-save adders plus one final carry-propagate add. At 32 or 64 bits that is still far more combinational depth than an adder of the same width, which is why the multiplier is usually the first thing a datapath pipelines.

Where to cut

Registering the inputs, multiplying combinationally and registering the output does not help: the multiply is still one large block between two registers. The cut has to go through the multiplier’s internal structure, at a point where a well-defined intermediate result exists.

For a hand-built Wallace tree, the natural cut is where Module 2 left off: after the carry-save tree has reduced the partial products to two rows, a sum row and a carry row, and before the final carry-propagate adder combines them. Both rows are the width of the product, so registering them costs two registers instead of one, and the second stage is “just” an adder that you already know how to pipeline further if needed.

Cut after the reduction tree, before the final adder
Figure 1: Cutting a multiplier between the reduction tree and the final adder

In behavioural RTL where the * operator stands in for the tree, the same idea can be expressed by splitting the multiplicand into halves. a * b is a * b[lo] + (a * b[hi] << N/2). Stage 1 computes the two narrower products, each with roughly half the depth of the full one; stage 2 adds them. The two partial products play the role of the sum and carry rows.

The 2-stage pipelined multiplier

`timescale 1ns / 1ps
`default_nettype none

module pipelined_mult #(parameter WIDTH = 16) (
    input  wire                      clk,
    input  wire [WIDTH-1:0]          a, b,
    input  wire                      valid_in,
    output reg  [2*WIDTH-1:0]        product,
    output reg                       valid_out
);
    localparam H = WIDTH / 2;

    // Stage 1: two half-width partial products. In a hand-built Wallace
    // tree these would be the sum row and carry row out of the reduction.
    reg [2*WIDTH-1:0] pp_lo, pp_hi;
    reg               valid_s1;

    always @(posedge clk) begin
        pp_lo    <= a * b[H-1:0];                 // a x lower half of b
        pp_hi    <= (a * b[WIDTH-1:H]) << H;      // a x upper half of b, shifted into place
        valid_s1 <= valid_in;
    end

    // Stage 2: final carry-propagate addition combining the two rows
    always @(posedge clk) begin
        product   <= pp_lo + pp_hi;
        valid_out <= valid_s1;
    end
endmodule

If you wire in your Module 2 tree instead, stage 1’s block captures the tree’s sum and carry rows directly and nothing else changes.

The valid signal is not optional

valid_in rides through the pipeline exactly like the carry in the previous article and the valid bits in Module 5. A multiplier usually sits inside a larger datapath that does not present a new operand pair every cycle; there are gaps and stalls. Without valid_out, downstream logic cannot tell the result of a multiply that happened two cycles ago from whatever was left in the pipeline register because nothing valid has flowed through yet.

Testbench: correctness and latency

module tb_pipelined_mult;
    parameter WIDTH = 8;
    reg clk = 0, valid_in;
    reg [WIDTH-1:0] a, b;
    wire [2*WIDTH-1:0] product;
    wire valid_out;

    pipelined_mult #(.WIDTH(WIDTH)) dut (
        .clk(clk), .a(a), .b(b), .valid_in(valid_in),
        .product(product), .valid_out(valid_out)
    );

    always #5 clk = ~clk;

    initial begin
        $dumpfile("tb.vcd");
        $dumpvars(0, tb_pipelined_mult);

        valid_in = 0; a = 0; b = 0;
        @(negedge clk);

        a = 8'd13; b = 8'd11; valid_in = 1;   // 13 * 11 = 143
        @(negedge clk);   // cycle 1: partial products captured
        valid_in = 0;
        @(negedge clk);   // cycle 2: product should now be valid

        if (product == 16'd143 && valid_out == 1'b1)
            $display("PASS: product=%0d arrived on cycle 2 with valid_out asserted", product);
        else
            $display("FAIL: product=%0d valid_out=%b (expected 143, 1)", product, valid_out);

        $finish;
    end
endmodule

As with the adder, the test is not only “is the number right” but “does it show up on the cycle the pipeline depth predicts, with valid_out set”.

FPGA vendors build their hard multiplier blocks (Xilinx DSP48, Intel DSP blocks) with optional pipeline registers at exactly these internal boundaries. When a synthesis tool infers a multiplier from * and sees a register on the output, it will usually map the whole thing onto one of those blocks rather than build it from general logic.

Practice Exercise

Extend the multiplier to 3 stages by pipelining the final adder with the split from the previous article, so a 16-bit multiply takes 3 cycles: one for the partial products, two for the split final addition. Confirm the valid signal threads through all three stages.

Solution
module pipelined_mult_3stage #(parameter WIDTH = 16) (
    input  wire               clk,
    input  wire [WIDTH-1:0]   a, b,
    input  wire               valid_in,
    output reg  [2*WIDTH-1:0] product,
    output reg                valid_out
);
    localparam H = WIDTH / 2;
    reg [2*WIDTH-1:0] pp_lo, pp_hi;
    reg               valid_s1;

    // Stage 1: partial products
    always @(posedge clk) begin
        pp_lo    <= a * b[H-1:0];
        pp_hi    <= (a * b[WIDTH-1:H]) << H;
        valid_s1 <= valid_in;
    end

    // Stage 2: lower half of the final add, capture the carry (same pattern as 07-02/03)
    reg [WIDTH-1:0]       sum_lo;
    reg                   carry_mid;
    reg [2*WIDTH-1:WIDTH] pp_lo_hi, pp_hi_hi;
    reg                   valid_s2;

    always @(posedge clk) begin
        {carry_mid, sum_lo} <= pp_lo[WIDTH-1:0] + pp_hi[WIDTH-1:0];
        pp_lo_hi <= pp_lo[2*WIDTH-1:WIDTH];
        pp_hi_hi <= pp_hi[2*WIDTH-1:WIDTH];
        valid_s2 <= valid_s1;
    end

    // Stage 3: upper half of the final add, using the registered carry
    always @(posedge clk) begin
        product[WIDTH-1:0]       <= sum_lo;
        product[2*WIDTH-1:WIDTH] <= pp_lo_hi + pp_hi_hi + carry_mid;
        valid_out <= valid_s2;
    end
endmodule

valid_in now passes through three registers, valid_s1, valid_s2 and valid_out, one per stage, mirroring the data. A testbench that applies an input on cycle 0 should see valid_out and the correct product on cycle 3, not before and not after.

A multiplier’s path is long enough, even after a Wallace reduction, that pipelining it is the expectation rather than an optimisation. The place to cut is between the reduction and the final add, and the valid bit threading through every stage is the only way downstream logic can tell a result from a register that has not seen valid data yet. That closes Section 7.2. In the next article, Section 7.3 generalises pipelining properly: how many stages, where to cut a whole datapath, and what to do when it has to stop.

โ† Pipelining a Wide Adder Balancing an N-Stage Pipeline โ†’