Module 5: Behavioral Verilog for Synthesis ยท 5.5 Modeling Flip-Flops and Registers

Pipeline Registers

A combinational path takes 8 ns to settle and the clock period is 5 ns. The flip-flop at the end samples before the logic has finished, which is a setup violation, and it captures a wrong answer with complete confidence. There are two ways out: slow the clock to 8 ns, or cut the path into pieces that each fit in 5 ns, with a register between them. The second is pipelining, and it is how designs reach their clock targets.

The trade

One worker who does every step of a job takes 8 hours per item. Four stations of 2 hours each finish an item every 2 hours once the line is full, four times the rate, and each item still spends 8 hours in the line. No item got faster; many items together did.

Throughput, results per unit time, goes up: one per clock once the pipeline is full, with the clock now set by the longest stage rather than the whole path. Latency, cycles from an input to its result, goes up: $N$ cycles for $N$ stages. You do not get both, and pipelining is the bet that a faster clock on a steady stream matters more than the delay on any one result.

A two-stage multiply-add

A multiply followed by an add is a long path. Split it:

// stage 1: the product
always @(posedge clk) begin
    product_s1 <= a * b;
end

// stage 2: the sum
always @(posedge clk) begin
    result_s2 <= product_s1 + accum;
end

An input applied in cycle 0 has its product registered at cycle 1 and its sum at cycle 2: two cycles of latency. But a second input applied in cycle 1 has its product at cycle 2, while the first input’s sum is also landing at cycle 2. The two overlap. Once the pipeline is full, a result comes out every cycle, each one two cycles after its input.

What is in the pipeline

During reset, and for the first cycles after it, product_s1 and result_s2 hold values nobody asked for. A valid bit travelling with the data says which stage contents mean anything:

always @(posedge clk) begin
    valid_s1 <= valid_in;
    valid_s2 <= valid_s1;
end

It is the data path again, one bit wide. Each cycle the bit moves one stage, in step with the data, and when result_s2 is read, valid_s2 says whether it is a result or leftover (Figure 1).

Data and valid through a pipeline
Figure 1: The valid bit shadows the data stage for stage

What it does not do

This pipeline cannot stop. If whatever consumes result_s2 is not ready every cycle, nothing here can hold the earlier stages; data keeps coming whether or not it is caught. Real pipelines add a ready signal flowing backwards and hold logic at each stage, which is a subject of its own in Module 7. Everything here assumes a consumer that takes a result every cycle.

Timing closure on an FPGA is largely a pipelining exercise. When a design misses its clock, the usual fix is a register at a well-chosen point on the longest path, and knowing pipelines at this level is what makes a timing report readable.

Practice Exercise

A three-input adder sums three 8-bit numbers in one combinational block. Restructure it as a two-stage pipeline, say on which cycle each intermediate and the result become valid, and test that the value appears on exactly that cycle.

Solution
`timescale 1ns / 1ps
`default_nettype none

module adder_tree_pipelined (
    input  wire       clk,
    input  wire [7:0] a, b, c,
    input  wire       valid_in,
    output reg  [8:0] sum_out,
    output reg        valid_out
);
    reg [8:0] ab_s1;
    reg       valid_s1;

    always @(posedge clk) begin          // stage 1: a + b
        ab_s1    <= a + b;
        valid_s1 <= valid_in;
    end

    always @(posedge clk) begin          // stage 2: + c
        sum_out   <= ab_s1 + c;
        valid_out <= valid_s1;
    end
endmodule

module tb_adder_tree_pipelined;
    reg        clk = 0, valid_in;
    reg  [7:0] a, b, c;
    wire [8:0] sum_out;
    wire       valid_out;

    adder_tree_pipelined dut (.clk(clk), .a(a), .b(b), .c(c), .valid_in(valid_in), .sum_out(sum_out), .valid_out(valid_out));

    always #5 clk = ~clk;

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

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

        a = 8'd10; b = 8'd20; c = 8'd30; valid_in = 1;
        @(negedge clk);        // after edge 1: ab_s1 = 30, valid_s1 = 1
        valid_in = 0;
        @(negedge clk);        // after edge 2: sum_out = 60, valid_out = 1

        if (sum_out !== 9'd60 || valid_out !== 1'b1)
            $display("FAIL: sum_out=%0d valid_out=%b (expected 60, 1)", sum_out, valid_out);
        else
            $display("PASS: sum_out=%0d on cycle 2", sum_out);

        $finish;
    end
endmodule

The check is not only that the sum is 60 but that it is 60 on the second edge and not before, when ab_s1 alone is there.

Note that c is added in stage 2 to the value of a + b from stage 1, so c has to be held one cycle, or registered alongside ab_s1; in this test it is held, and in a real design you would register it.

That closes the register section. Section 5.6 puts the whole module together into the most important structure in digital design: the state machine, in RTL.

โ† Register Patterns The State Machine in RTL โ†’