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

Pipelining a Wide Adder

Module 5 introduced the general pipelining pattern: a register between combinational stages, trading latency for throughput. This article is the specific case where you actually need it most often, a 64-bit adder whose carry chain cannot settle inside one clock period at the frequency you are targeting.

You know from Module 2 why this happens. The carry ripples, or at best resolves through a lookahead tree, and either way there is real gate delay between the inputs and a trustworthy output. The fix this time is not a faster adder architecture. It is breaking the one adder into two half-width adders separated by a pipeline register, so the timing tool only has to close half the carry chain per cycle.

The wrong way to split an adder

A first instinct is to register the operands halfway and run two 32-bit adds:

// WRONG: this does not add the two halves correctly across cycles
always @(posedge clk) begin
    a_hi_reg <= a[63:32];
    b_hi_reg <= b[63:32];
    sum_lo   <= a[31:0] + b[31:0];   // lower half computed this cycle
end

always @(posedge clk) begin
    sum_hi <= a_hi_reg + b_hi_reg;   // upper half computed next cycle, but where is the carry?
end

The upper half needs the carry-out of the lower half. That carry is produced at the end of the first stage, and by the time the second stage runs it has to be sitting in a register, ready to be the carry-in of the upper adder. The code above never captures it. This “pipelined” adder produces the wrong sum on every input where the lower half carries out.

The correct split: register the carry, not only the operands

`timescale 1ns / 1ps
`default_nettype none

module pipelined_adder_64 (
    input  wire        clk,
    input  wire [63:0] a, b,
    output reg  [63:0] sum,
    output reg         cout
);
    // Stage 1: compute the lower 32 bits AND capture the carry it produces
    reg [31:0] sum_lo_reg;
    reg        carry_reg;
    reg [63:32] a_hi_reg, b_hi_reg;

    always @(posedge clk) begin
        {carry_reg, sum_lo_reg} <= a[31:0] + b[31:0];
        a_hi_reg <= a[63:32];
        b_hi_reg <= b[63:32];
    end

    // Stage 2: add the upper 32 bits, using the REGISTERED carry from stage 1
    always @(posedge clk) begin
        {cout, sum[63:32]} <= a_hi_reg + b_hi_reg + carry_reg;
        sum[31:0] <= sum_lo_reg;   // the lower sum rides along unchanged, to stay aligned
    end
endmodule

Two things do the real work. carry_reg is captured in the same pipeline register as sum_lo_reg: the carry out of the lower half is a piece of data that has to cross the boundary like the sum bits do. And sum_lo_reg rides through the second stage unchanged, purely to stay time-aligned with sum[63:32], which is ready one cycle later. Miss that and you assemble a sum whose two halves came from different additions.

Pipelined adder: the carry crosses the register boundary with the lower sum
Figure 1: Pipelined adder with the carry registered between stages

Why this buys speed

Each stage now only has to close timing on a 32-bit carry chain instead of a 64-bit one. If the chain’s delay scales roughly with width (true for ripple, and true in a softer sense for lookahead structures once wiring and fan-out are counted), halving the width comes close to halving the worst-case delay in the stage. That is the headroom that lets you raise the clock, at the cost of the result arriving one cycle later than an unpipelined add would deliver it.

This is the throughput-versus-latency trade from Module 5 applied to one common piece of hardware. Any adder wide enough for its carry chain to be the critical path is a candidate for this split, and nothing stops you splitting it further: a 64-bit adder can become four pipelined 16-bit stages, each carry captured and passed forward the same way. Server-class ALUs do exactly this, typically with a prefix adder inside each stage rather than the plain ripple shown here.

Practice Exercise

Extend the pipelined adder from a 2-stage, 32+32 split into a 4-stage, 16+16+16+16 split. Write a testbench that applies an addition whose carry has to propagate from bit 0 to bit 63, and confirm the correct sum appears exactly 4 cycles after the inputs.

Solution

Every operand slice is threaded forward register to register, the same way as the carry. Re-sampling the original input bus a cycle late would only work if a and b stayed stable for the whole pipeline fill, which a real design cannot assume.

`timescale 1ns / 1ps
`default_nettype none

module pipelined_adder_64_4stage (
    input  wire        clk,
    input  wire [63:0] a, b,
    output reg  [63:0] sum,
    output reg         cout
);
    // Stage 1 registers
    reg [15:0] s1_sum0;            reg s1_c;
    reg [47:0] s1_a_rest, s1_b_rest;
    // Stage 2 registers
    reg [31:0] s2_sum;             reg s2_c;
    reg [31:0] s2_a_rest, s2_b_rest;
    // Stage 3 registers
    reg [47:0] s3_sum;             reg s3_c;
    reg [15:0] s3_a_rest, s3_b_rest;

    always @(posedge clk) begin
        // Stage 1: bits [15:0]
        {s1_c, s1_sum0} <= a[15:0] + b[15:0];
        s1_a_rest <= a[63:16]; s1_b_rest <= b[63:16];

        // Stage 2: bits [31:16]
        {s2_c, s2_sum[31:16]} <= s1_a_rest[15:0] + s1_b_rest[15:0] + s1_c;
        s2_sum[15:0] <= s1_sum0;
        s2_a_rest <= s1_a_rest[47:16]; s2_b_rest <= s1_b_rest[47:16];

        // Stage 3: bits [47:32]
        {s3_c, s3_sum[47:32]} <= s2_a_rest[15:0] + s2_b_rest[15:0] + s2_c;
        s3_sum[31:0] <= s2_sum;
        s3_a_rest <= s2_a_rest[31:16]; s3_b_rest <= s2_b_rest[31:16];

        // Stage 4: bits [63:48]
        {cout, sum[63:48]} <= s3_a_rest + s3_b_rest + s3_c;
        sum[47:0] <= s3_sum;
    end
endmodule

The testbench applies a = 64'hFFFF_FFFF_FFFF_FFFF and b = 64'h0000_0000_0000_0001, a carry that has to cross every stage boundary, and checks that sum == 64'h0 with cout == 1 exactly 4 clock edges after the inputs were driven. That checks both the carry threading and the pipeline latency.

Pipelining a wide adder is more than registering the operands at the midpoint. The carry out of the earlier stage must be captured and fed forward as the carry in of the later one, and every already-computed sum bit must ride along so the halves stay aligned. Get the carry wrong and the adder is right on most inputs and wrong on exactly the ones that carry across the cut. In the next article we apply the same discipline to something with a much longer critical path to begin with, a multiplier.

โ† Saturating Arithmetic Pipelining a Multiplier โ†’