Skip to content

RTL Design Patterns · Chapter 8 · Pipelining

Pipelined Datapath Worked Example

This capstone assembles everything Chapter 8 taught into one working block: a pipelined multiply-accumulate that takes a new triple of operands every cycle and streams a times b plus c results. Done as one long combinational path the multiply-then-add is too slow to close timing, so you split it into two balanced stages, a multiply stage and an add stage. Two cycles of latency buys one result per cycle of throughput. Registering the datapath brings two obligations: a valid bit must march alongside the data, and every operand that meets at a stage must be delayed by the same number of stages, so c is registered one stage to line up with its own product. This lesson builds the block incrementally, adds stall and flush, and proves it against a latency-delayed reference in SystemVerilog, Verilog, and VHDL.

Intermediate16 min readRTL Design PatternsPipeliningMultiply-AccumulateValid BitOperand AlignmentStall and Flush

Chapter 8 · Section 8.6 · Pipelining

1. The Engineering Problem

You are building a small compute engine that lands everywhere in signal processing and datapath accelerators: a multiply-accumulate. Every cycle a producer hands you a fresh triple — a, b, c — and every cycle you must emit a*b + c. A FIR filter tap does this; a dot-product does this; a running weighted sum does this. There is nothing exotic about the arithmetic — one multiply and one add — and the naive way to write it is exactly one line: result = a*b + c, a combinational multiply feeding a combinational add.

That one line is the problem. A multiplier is not a cheap gate; it is a deep tree of partial-product adders whose delay grows with the operand width. Bolt an adder on its output and you have chained the two longest arithmetic operations you own into a single combinational path from the input registers to the output register. At any real clock speed that path does not close timing — the multiply alone eats most of the period, and the trailing add pushes it over. You cannot ship a datapath whose critical path is a*b + c computed in one hop, and slowing the clock until it fits throws away exactly the throughput the rest of the chip was designed around (this is the timing pressure of 8.1).

The escape is the whole of Chapter 8. You pipeline: cut that long path in two with a register in the middle, so the multiply gets its own cycle and the add gets its own cycle. Now each stage's path is roughly half as long and the clock can run fast again. But cutting the path is only the first move — a registered datapath streams many operations at once, and that raises three obligations the naive one-liner never had: the consumer must be told which outputs are real (the pipe starts empty), the operations must be kept from colliding under backpressure (stall and flush), and — the trap this page turns on — the c you add must belong to the same operation as the product it is added to. This page builds that block, end to end, and the interface it presents is small.

the-interface.v — the whole contract: a stream of triples in, a stream of a*b+c out
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // INPUTS  (a fresh operation offered every cycle)
   input  wire              clk;
   input  wire              rst;        // synchronous, active-high -> pipe empty, valids clear
   input  wire              in_valid;   // this cycle's a,b,c are a REAL operation (else a bubble)
   input  wire [WIDTH-1:0]  a, b, c;    // operands: compute a*b + c for this triple
 
   input  wire              stall;      // backpressure: FREEZE the whole pipe this cycle
   input  wire              flush;      // drop in-flight work: clear the valid bits
 
   // OUTPUTS (the result of the operation offered LATENCY cycles ago)
   output wire [2*WIDTH-1:0] result;    // a*b + c for the matching operation, LSBs aligned
   output wire               out_valid; // this result is REAL (not a start-up / flushed bubble)
 
   // Timing: multiply in stage 1, add in stage 2 -> LATENCY = 2 cycles.
   //   result[t] corresponds to the triple offered at t-2 (when in_valid was high).
   //   Throughput = 1 result/cycle once the pipe is full (8.2).

2. Mental Model

3. Pattern Anatomy

The block is two compute stages, a valid chain that shadows them, one operand that must be delayed to stay aligned, and two controls that act on the whole region. Take them in turn, then see the two views that matter: the compute-stage datapath (who feeds whom) and the control overlay (how valid marches and a stall freezes).

The two compute stages — the split path. Stage 1 is the multiplier from 5.x: prod_q <= a * b, a 2*WIDTH-bit product captured in a pipeline register. Stage 2 is the adder from 5.5 / adders-subtractors: result <= prod_q + c_q, the product plus the (aligned) addend, captured in the output register. The register between them — prod_q — is the cut that breaks the long a*b + c path into two shorter ones. Before the cut, the critical path was multiply-then-add; after it, stage 1 is just the multiply and stage 2 is just the add, each about half the delay, which is why the clock can run fast (8.1).

The valid chain — the work order that shadows the data. A one-bit register per stage — v1_q, v2_q — carries in_valid down the pipe in lockstep with the data: v1_q <= in_valid, then v2_q <= v1_q, and out_valid = v2_q. Because the valid bit lives in the same number of registers as the data it labels, it arrives at the output on the exact cycle the matching result does. This is the valid-bit discipline of 8.3: the data path carries the value, the valid path carries the fact that the value is real, and they must move together.

The aligned operand — c delayed one stage. Here is the crux. The product prod_q is a*b from one cycle ago (it spent a cycle in stage 1's register). The adder in stage 2 must add that operation's c — which was presented one cycle ago, together with its a and b. So c cannot go straight to the adder; it must be registered one stage (c_q <= c) so it arrives at the adder on the same cycle its product does. Every operand that converges at a pipeline stage has to be delayed by the same number of stages as everything else it meets there — c meets a one-stage-old product, so c must be one stage old too.

Stall — freeze the whole region. Backpressure (a downstream consumer that cannot take a result this cycle) must not drop or duplicate in-flight work. The clean answer is a clock-enable: when stall is high, none of the pipeline registers update — prod_q, c_q, v1_q, v2_q, and result all hold. Freeze the whole stage region as a unit, and the operations already in flight simply pause where they are and resume intact when the stall lifts. Freezing only part of the region is a bug (§6).

Flush — clear the valids. When in-flight work must be abandoned (a mispredict, a mode change, a reset of the stream), you do not need to clear the data registers — garbage data that is marked not valid is harmless. You only clear the valid chain: flush forces v1_q and v2_q to 0, so whatever is in the data registers stops being reported as real. Flush is cheap precisely because valid, not data, is what makes an output count.

Compute-stage datapath — two balanced stages, and c delayed one stage to meet its product

data flow
Compute-stage datapath — two balanced stages, and c delayed one stage to meet its producta, bcprod_q (1 stage)c_q (1 stage)suminputs a, b, ca fresh triple offered each cycle (when in_valid)STAGE 1 —multiplyprod_q <= a * b (the 5.x multiplier, registered)c delay — c_qc_q <= c: register c ONE stage so it aligns with prodSTAGE 2 — addresult <= prod_q + c_q (the 5.5 adder, registered)result (a*b + c)the matching operation, LATENCY = 2 cycles later
The long a*b + c path is cut in two: stage 1 registers the product prod_q = a*b, stage 2 registers the sum result = prod_q + c_q. The single register between them is what shortens the critical path from multiply-THEN-add to just multiply (stage 1) and just add (stage 2). The load-bearing detail is the c path: because prod_q is a product from one cycle ago, the c it is added to must ALSO be one cycle old, so c is registered through a delay (c_q <= c) to reach the adder aligned with its own product. Feed c straight to the adder instead and each result mixes a product from one operation with the addend from the next (the bug in the DebugLab). Identical in structure across SystemVerilog, Verilog, and VHDL.

The second view is the control overlay — the valid bit marching alongside the data, and a stall freezing the whole region.

Control overlay — valid marches with the data, stall freezes the region, flush clears the valids

data flow
Control overlay — valid marches with the data, stall freezes the region, flush clears the validsclock inadvance inlockstepreport realfreeze /clearfreeze /clearin_valid1 = this cycle's triple is a REAL operationv1_q (stage 1valid)v1_q <= in_valid — rides with prod_q and c_qv2_q (stage 2valid)v2_q <= v1_q — rides with resultout_validout_valid = v2_q — high exactly on real resultsstall / flushstall freezes ALL regs; flush clears v1_q, v2_q
The valid bit shadows the data one-for-one: in_valid enters, becomes v1_q alongside prod_q and c_q, becomes v2_q alongside result, and emerges as out_valid — so the consumer sees a high valid exactly on the cycles result is a genuine a*b + c and a low valid while the pipe is filling. Two controls act on the whole region: stall is a clock-enable that freezes EVERY pipeline register (data AND valid) so in-flight work pauses intact and resumes when stall lifts; flush forces the valid chain to 0 so any in-flight work stops being reported as real, without touching the (now-harmless) data registers. Latency is 2 cycles; throughput is one result per cycle once the pipe is full. The same valid chain, freeze, and clear appear in all three HDLs.

One reasoning thread runs under both views: the data path, the valid path, and every meeting operand advance together, one register per stage, and stall/flush act on all of them at once. On a normal cycle each stage's register captures the previous stage's output, the valid bit steps forward one register, and c_q steps forward the one stage it needs to meet its product. On a stall the whole region holds — data and valid alike — so nothing is lost or duplicated. On a flush the valid chain zeroes so the in-flight garbage stops counting. The only subtlety worth losing sleep over is alignment: every value that arrives at the stage-2 adder must have travelled the same number of registers to get there, and the one that is easy to forget is c (§7).

4. Real RTL Implementation

This is the core of the page. We build the pipelined MAC incrementally — (i) the plain 2-stage compute pipeline, (ii) add the per-stage valid bit, (iii) add stall and flush — then present the integrated module in SystemVerilog, Verilog, and VHDL, each with a self-checking clocked testbench that streams random a, b, c (with bubbles, a stall, and a flush) and checks result against a latency-delayed a*b + c reference and out_valid alignment. Then §4d isolates the signature bug — the operand-misalignment of an un-delayed c — as buggy-vs-fixed RTL in all three languages.

Three conventions run through every block, and each is a Chapter-8 lesson made concrete:

  • Reset is synchronous, active-high (rst), emptying the pipe by clearing the valid chain (the data registers may hold anything — cleared valids make them harmless). After reset out_valid is low until real operations have propagated through.
  • A pipeline register sits between the stages, and every operand that meets at a stage is delayed the same number of stages. prod_q cuts the path; c_q delays c one stage so it reaches the adder aligned with its product (the alignment discipline).
  • The valid bit shadows the data one register per stage, and stall is a clock-enable on the whole region while flush clears only the valid chain — the 8.3 valid-bit discipline plus backpressure handling.

4a. Incremental build — the plain 2-stage compute pipeline

Start with just the arithmetic, cut in two. Stage 1 registers the product; stage 2 registers the sum. Even here the alignment rule already applies: c is registered once (c_q) so stage 2 adds the c that belongs to the product it has.

mac_pipe_v1.sv — step (i): plain 2-stage compute pipeline (no valid yet), c aligned
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module mac_pipe_v1 #(
       parameter int WIDTH = 8                       // operand width; product is 2*WIDTH
   )(
       input  logic                 clk,
       input  logic                 rst,             // synchronous, active-high
       input  logic [WIDTH-1:0]     a, b, c,         // a fresh triple each cycle
       output logic [2*WIDTH-1:0]   result           // a*b + c, LATENCY = 2 cycles
   );
       logic [2*WIDTH-1:0] prod_q;                   // STAGE-1 register: the product a*b
       logic [WIDTH-1:0]   c_q;                      // c delayed ONE stage to align with prod_q
 
       always_ff @(posedge clk) begin
           if (rst) begin
               prod_q <= '0;
               c_q    <= '0;
               result <= '0;
           end else begin
               // STAGE 1: multiply, and delay c by the SAME one stage so they meet aligned.
               prod_q <= a * b;                      // 5.x multiplier, registered
               c_q    <= c;                          // c_q holds THIS operation's addend
               // STAGE 2: add the product to its OWN (one-stage-old) addend.
               result <= prod_q + c_q;               // 5.5 adder, registered
           end
       end
   endmodule

The two right-hand sides read the current register values and the new inputs, and all assignments are non-blocking, so on each edge stage 2 consumes the prod_q/c_q that stage 1 produced last cycle while stage 1 loads the new product and addend — exactly the assembly-line hand-off. result is a*b + c for the triple offered two cycles earlier. Nothing here yet tells the consumer which outputs are real; that is step (ii).

4b. Incremental build — add the per-stage valid bit

Now attach the work order. A one-bit register per stage carries in_valid down the pipe in lockstep with the data, and out_valid reports the stage-2 valid. This is the whole of 8.3, and it is one register wide.

mac_pipe_v2.sv — step (ii): valid bit shadows the data, one register per stage
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module mac_pipe_v2 #(
       parameter int WIDTH = 8
   )(
       input  logic                 clk,
       input  logic                 rst,
       input  logic                 in_valid,        // this triple is a real operation
       input  logic [WIDTH-1:0]     a, b, c,
       output logic [2*WIDTH-1:0]   result,
       output logic                 out_valid        // result is a real a*b + c this cycle
   );
       logic [2*WIDTH-1:0] prod_q;
       logic [WIDTH-1:0]   c_q;
       logic               v1_q, v2_q;               // valid, one bit per stage
 
       always_ff @(posedge clk) begin
           if (rst) begin
               v1_q <= 1'b0; v2_q <= 1'b0;           // empty the pipe: clear the valid chain
               prod_q <= '0; c_q <= '0; result <= '0;
           end else begin
               // DATA path (as 4a) AND valid path advance TOGETHER, register for register.
               prod_q <= a * b;      v1_q <= in_valid;   // stage 1: product + its valid
               c_q    <= c;                              //          c aligned one stage
               result <= prod_q + c_q; v2_q <= v1_q;     // stage 2: sum + its valid
           end
       end
       assign out_valid = v2_q;                      // high exactly on real results
   endmodule

v1_q rides with prod_q and c_q; v2_q rides with result. Because valid lives in the same number of registers as the data it labels, out_valid is high precisely on the cycles result is a genuine a*b + c — and low for the two-cycle fill after reset. If you advanced v2_q <= in_valid (skipping a stage) the label would arrive a cycle early and mark a fill bubble as real; the valid chain must be as deep as the data (§6).

4c. Incremental build — add stall (clock-enable freeze) and flush

Finally, backpressure. stall is a clock-enable that freezes every register in the region; flush clears the valid chain so in-flight work stops counting. The freeze covers data and valid alike, so a paused operation resumes intact.

mac_pipe_v3.sv — step (iii): stall freezes the whole region; flush clears the valids
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module mac_pipe_v3 #(
       parameter int WIDTH = 8
   )(
       input  logic                 clk, rst,
       input  logic                 in_valid,
       input  logic [WIDTH-1:0]     a, b, c,
       input  logic                 stall,           // freeze the whole pipe this cycle
       input  logic                 flush,           // drop in-flight work (clear valids)
       output logic [2*WIDTH-1:0]   result,
       output logic                 out_valid
   );
       logic [2*WIDTH-1:0] prod_q;
       logic [WIDTH-1:0]   c_q;
       logic               v1_q, v2_q;
 
       always_ff @(posedge clk) begin
           if (rst) begin
               v1_q <= 1'b0; v2_q <= 1'b0;
               prod_q <= '0; c_q <= '0; result <= '0;
           end else if (!stall) begin               // CLOCK-ENABLE: update only when NOT stalled
               prod_q <= a * b;
               c_q    <= c;
               result <= prod_q + c_q;
               // flush clears the valid chain; otherwise valid advances with the data.
               v1_q   <= flush ? 1'b0 : in_valid;
               v2_q   <= flush ? 1'b0 : v1_q;
           end
           // when stall is high: NO register updates -> the whole region holds intact.
       end
       assign out_valid = v2_q;
   endmodule

Two decisions carry the semantics. The if (!stall) gates all the register updates as one unit, so a stalled cycle freezes prod_q, c_q, v1_q, v2_q, and result together — no operation is dropped or duplicated. The flush ? 1'b0 : ... clears only the valid chain, leaving the data registers alone: garbage marked not-valid is harmless, so flush is cheap. This mac_pipe_v3 is the integrated block; the testbench below drives it with bubbles, a stall, and a flush.

mac_pipe_tb.sv — clocked self-check: stream a,b,c with bubbles + stall + flush; assert vs latency-delayed reference
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module mac_pipe_tb;
       localparam int WIDTH = 8;
       localparam int LAT   = 2;                      // pipeline latency (stages)
       logic clk = 1'b0, rst, in_valid, stall, flush, out_valid;
       logic [WIDTH-1:0]     a, b, c;
       logic [2*WIDTH-1:0]   result;
       int errors = 0;
 
       mac_pipe_v3 #(.WIDTH(WIDTH)) dut (.clk(clk), .rst(rst), .in_valid(in_valid),
           .a(a), .b(b), .c(c), .stall(stall), .flush(flush),
           .result(result), .out_valid(out_valid));
 
       always #5 clk = ~clk;
 
       // Reference model: a shift-register of expected {value, valid} delayed by LAT stages,
       // advanced ONLY on non-stall cycles so it tracks the DUT's clock-enable exactly.
       logic [2*WIDTH-1:0] ref_val [LAT+1];
       logic               ref_vld [LAT+1];
 
       task automatic step(input logic iv, st, fl, input [WIDTH-1:0] xa, xb, xc);
           in_valid = iv; stall = st; flush = fl; a = xa; b = xb; c = xc;
           @(posedge clk); #1;
           if (!st) begin                             // model advances only when not stalled
               for (int i = LAT; i > 0; i--) begin
                   ref_val[i] = ref_val[i-1];
                   ref_vld[i] = fl ? 1'b0 : ref_vld[i-1];   // flush clears in-flight valids
               end
               ref_val[0] = xa * xb + xc;             // the operation entering this cycle
               ref_vld[0] = fl ? 1'b0 : iv;
           end
           // Check the DUT output against the model at stage LAT.
           assert (out_valid === ref_vld[LAT])
               else begin $error("valid mismatch: out_valid=%b exp=%b", out_valid, ref_vld[LAT]); errors++; end
           if (ref_vld[LAT])                          // only REAL results must match
               assert (result === ref_val[LAT])
                   else begin $error("result mismatch: got=%h exp=%h", result, ref_val[LAT]); errors++; end
       endtask
 
       initial begin
           rst = 1'b1; in_valid = 0; stall = 0; flush = 0; a = 0; b = 0; c = 0;
           foreach (ref_vld[i]) ref_vld[i] = 1'b0;
           @(posedge clk); #1; rst = 1'b0;
 
           // Stream real ops with a bubble, a stall, and a flush interleaved. A VARYING c
           // stream is what would expose the misalignment bug of 4d.
           step(1,0,0, 8'd2, 8'd4, 8'd1  );           // a*b+c = 9
           step(1,0,0, 8'd3, 8'd6, 8'd10 );           //         28
           step(0,0,0, 8'd0, 8'd0, 8'd0  );           // bubble (in_valid = 0)
           step(1,1,0, 8'd5, 8'd7, 8'd100);           // STALL: pipe freezes, offer held
           step(1,0,0, 8'd5, 8'd7, 8'd100);           //         135 (accepted after stall lifts)
           step(1,0,1, 8'd9, 8'd9, 8'd9  );           // FLUSH: in-flight valids cleared
           step(0,0,0, 8'd0, 8'd0, 8'd0  );           // drain
           step(0,0,0, 8'd0, 8'd0, 8'd0  );
           step(0,0,0, 8'd0, 8'd0, 8'd0  );
 
           if (errors == 0) $display("PASS: stream matches latency-delayed a*b+c; valid aligned through bubble/stall/flush");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

4b (Verilog). The integrated block in Verilog

The Verilog form is the same three-stage-register machine with reg/wire typing; the incremental steps collapse into the one integrated module since the reasoning is identical. prod_q, c_q, and the valid chain advance together, gated by !stall, cleared by flush.

mac_pipe.v — integrated pipelined MAC in Verilog (c aligned, valid chain, stall + flush)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module mac_pipe #(
       parameter WIDTH = 8
   )(
       input  wire               clk, rst,
       input  wire               in_valid,
       input  wire [WIDTH-1:0]   a, b, c,
       input  wire               stall, flush,
       output reg  [2*WIDTH-1:0] result,
       output wire               out_valid
   );
       reg [2*WIDTH-1:0] prod_q;         // stage-1 product register
       reg [WIDTH-1:0]   c_q;            // c delayed one stage to align with prod_q
       reg               v1_q, v2_q;     // valid, one bit per stage
 
       always @(posedge clk) begin
           if (rst) begin
               prod_q <= {(2*WIDTH){1'b0}};
               c_q    <= {WIDTH{1'b0}};
               result <= {(2*WIDTH){1'b0}};
               v1_q   <= 1'b0;
               v2_q   <= 1'b0;
           end else if (!stall) begin    // clock-enable: freeze whole region when stalled
               prod_q <= a * b;          // STAGE 1
               c_q    <= c;              //          c aligned
               result <= prod_q + c_q;   // STAGE 2: product + its OWN one-stage-old addend
               v1_q   <= flush ? 1'b0 : in_valid;
               v2_q   <= flush ? 1'b0 : v1_q;
           end
       end
       assign out_valid = v2_q;
   endmodule
mac_pipe_tb.v — self-checking Verilog TB: reference shift-register, print PASS/FAIL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module mac_pipe_tb;
       parameter WIDTH = 8;
       parameter LAT   = 2;
       reg               clk, rst, in_valid, stall, flush;
       reg  [WIDTH-1:0]  a, b, c;
       wire [2*WIDTH-1:0] result;
       wire              out_valid;
       integer           i, errors;
 
       // Reference model: LAT-deep shift registers of expected value and valid.
       reg  [2*WIDTH-1:0] ref_val [0:LAT];
       reg                ref_vld [0:LAT];
 
       mac_pipe #(.WIDTH(WIDTH)) dut (.clk(clk), .rst(rst), .in_valid(in_valid),
           .a(a), .b(b), .c(c), .stall(stall), .flush(flush),
           .result(result), .out_valid(out_valid));
 
       initial clk = 1'b0;
       always #5 clk = ~clk;
 
       task step;
           input iv, st, fl;
           input [WIDTH-1:0] xa, xb, xc;
           begin
               in_valid = iv; stall = st; flush = fl; a = xa; b = xb; c = xc;
               @(posedge clk); #1;
               if (!st) begin                         // model advances only when not stalled
                   for (i = LAT; i > 0; i = i - 1) begin
                       ref_val[i] = ref_val[i-1];
                       ref_vld[i] = fl ? 1'b0 : ref_vld[i-1];
                   end
                   ref_val[0] = xa * xb + xc;
                   ref_vld[0] = fl ? 1'b0 : iv;
               end
               if (out_valid !== ref_vld[LAT]) begin
                   $display("FAIL: valid out=%b exp=%b", out_valid, ref_vld[LAT]);
                   errors = errors + 1;
               end
               if (ref_vld[LAT] && result !== ref_val[LAT]) begin
                   $display("FAIL: result got=%h exp=%h", result, ref_val[LAT]);
                   errors = errors + 1;
               end
           end
       endtask
 
       initial begin
           errors = 0;
           rst = 1'b1; in_valid = 0; stall = 0; flush = 0; a = 0; b = 0; c = 0;
           for (i = 0; i <= LAT; i = i + 1) ref_vld[i] = 1'b0;
           @(posedge clk); #1; rst = 1'b0;
 
           step(1,0,0, 8'd2, 8'd4, 8'd1  );           // 9
           step(1,0,0, 8'd3, 8'd6, 8'd10 );           // 28   (varying c exposes misalignment)
           step(0,0,0, 8'd0, 8'd0, 8'd0  );           // bubble
           step(1,1,0, 8'd5, 8'd7, 8'd100);           // stall
           step(1,0,0, 8'd5, 8'd7, 8'd100);           // 135
           step(1,0,1, 8'd9, 8'd9, 8'd9  );           // flush
           step(0,0,0, 8'd0, 8'd0, 8'd0  );
           step(0,0,0, 8'd0, 8'd0, 8'd0  );
           step(0,0,0, 8'd0, 8'd0, 8'd0  );
 
           if (errors == 0) $display("PASS: stream matches a*b+c reference; valid aligned through bubble/stall/flush");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

4c (VHDL). The integrated block in VHDL

VHDL states the same pipeline with numeric_std for the arithmetic and rising_edge(clk) for the clocked process. prod_q and c_q are unsigned; the valid chain is two std_logic registers. The clock-enable is elsif stall = '0'; flush clears the valids.

mac_pipe.vhd — integrated pipelined MAC in VHDL (numeric_std, c aligned, valid chain, stall + flush)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity mac_pipe is
       generic ( WIDTH : positive := 8 );                     -- product is 2*WIDTH
       port (
           clk       : in  std_logic;
           rst       : in  std_logic;                         -- synchronous, active-high
           in_valid  : in  std_logic;
           a, b, c   : in  std_logic_vector(WIDTH-1 downto 0);
           stall     : in  std_logic;                         -- freeze whole pipe
           flush     : in  std_logic;                         -- clear in-flight valids
           result    : out std_logic_vector(2*WIDTH-1 downto 0);
           out_valid : out std_logic
       );
   end entity;
 
   architecture rtl of mac_pipe is
       signal prod_q : unsigned(2*WIDTH-1 downto 0);          -- stage-1 product register
       signal c_q    : unsigned(WIDTH-1 downto 0);            -- c delayed one stage to align
       signal res_q  : unsigned(2*WIDTH-1 downto 0);          -- stage-2 sum register
       signal v1_q, v2_q : std_logic;                         -- valid, one bit per stage
   begin
       pipe : process (clk)
       begin
           if rising_edge(clk) then
               if rst = '1' then
                   prod_q <= (others => '0');
                   c_q    <= (others => '0');
                   res_q  <= (others => '0');
                   v1_q   <= '0';
                   v2_q   <= '0';
               elsif stall = '0' then                         -- clock-enable: hold when stalled
                   -- STAGE 1: product, and c aligned by the SAME one stage.
                   prod_q <= unsigned(a) * unsigned(b);
                   c_q    <= unsigned(c);
                   -- STAGE 2: product + its OWN one-stage-old addend (zero-extend c_q).
                   res_q  <= prod_q + resize(c_q, 2*WIDTH);
                   -- valid advances with the data; flush clears the chain.
                   if flush = '1' then
                       v1_q <= '0'; v2_q <= '0';
                   else
                       v1_q <= in_valid; v2_q <= v1_q;
                   end if;
               end if;
           end if;
       end process;
 
       result    <= std_logic_vector(res_q);
       out_valid <= v2_q;
   end architecture;
mac_pipe_tb.vhd — self-checking VHDL TB: reference model, assert vs latency-delayed a*b+c (severity error)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity mac_pipe_tb is
   end entity;
 
   architecture sim of mac_pipe_tb is
       constant WIDTH : positive := 8;
       constant LAT   : positive := 2;
       signal clk       : std_logic := '0';
       signal rst       : std_logic;
       signal in_valid  : std_logic := '0';
       signal a, b, c   : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
       signal stall     : std_logic := '0';
       signal flush     : std_logic := '0';
       signal result    : std_logic_vector(2*WIDTH-1 downto 0);
       signal out_valid : std_logic;
 
       type   val_arr is array (0 to LAT) of unsigned(2*WIDTH-1 downto 0);
       type   vld_arr is array (0 to LAT) of std_logic;
   begin
       dut : entity work.mac_pipe
           generic map (WIDTH => WIDTH)
           port map (clk => clk, rst => rst, in_valid => in_valid, a => a, b => b, c => c,
                     stall => stall, flush => flush, result => result, out_valid => out_valid);
 
       clk <= not clk after 5 ns;
 
       stim : process
           variable ref_val : val_arr := (others => (others => '0'));
           variable ref_vld : vld_arr := (others => '0');
           variable errors  : integer := 0;
 
           -- One driven cycle: apply stimulus, advance the reference model on non-stall, check.
           procedure step (iv, st, fl : std_logic; xa, xb, xc : integer) is
               variable prod : unsigned(2*WIDTH-1 downto 0);
           begin
               in_valid <= iv; stall <= st; flush <= fl;
               a <= std_logic_vector(to_unsigned(xa, WIDTH));
               b <= std_logic_vector(to_unsigned(xb, WIDTH));
               c <= std_logic_vector(to_unsigned(xc, WIDTH));
               wait until rising_edge(clk); wait for 1 ns;
               if st = '0' then                               -- model advances only when not stalled
                   for i in LAT downto 1 loop
                       ref_val(i) := ref_val(i-1);
                       if fl = '1' then ref_vld(i) := '0';
                       else ref_vld(i) := ref_vld(i-1); end if;
                   end loop;
                   prod := to_unsigned(xa * xb + xc, 2*WIDTH);
                   ref_val(0) := prod;
                   if fl = '1' then ref_vld(0) := '0'; else ref_vld(0) := iv; end if;
               end if;
               assert out_valid = ref_vld(LAT)
                   report "valid mismatch" severity error;
               if ref_vld(LAT) = '1' then
                   assert unsigned(result) = ref_val(LAT)
                       report "result mismatch vs a*b+c reference" severity error;
               end if;
           end procedure;
       begin
           rst <= '1'; wait until rising_edge(clk); wait for 1 ns; rst <= '0';
 
           step('1','0','0', 2, 4, 1);                        -- 9
           step('1','0','0', 3, 6, 10);                       -- 28  (varying c exposes misalignment)
           step('0','0','0', 0, 0, 0);                        -- bubble
           step('1','1','0', 5, 7, 100);                      -- stall
           step('1','0','0', 5, 7, 100);                      -- 135
           step('1','0','1', 9, 9, 9);                        -- flush
           step('0','0','0', 0, 0, 0);
           step('0','0','0', 0, 0, 0);
           step('0','0','0', 0, 0, 0);
 
           report "mac_pipe self-check complete (stream matched a*b+c; valid aligned)" severity note;
           wait;
       end process;
   end architecture;

4d. The signature bug — operand misalignment (c un-delayed vs registered one stage)

Every failure of a pipelined datapath lives in the alignment of operands that meet at a stage, and the sharpest example is c. The buggy version feeds c straight to the adder instead of registering it one stage; the fix registers c (c_q) so it reaches the adder aligned with its own product. The bug is dangerous because it looks right: the pipeline still streams, out_valid is still perfect, and a steady c hides it completely — only a varying c stream exposes that each result mixes a product from one operation with the addend from the next.

mac_align_bug.sv — BUGGY (c un-delayed) vs FIXED (c registered one stage)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: c goes STRAIGHT to the stage-2 adder, un-delayed. prod_q is a*b from the
   //   PREVIOUS cycle, but c is THIS cycle's c -> result = a[t-1]*b[t-1] + c[t]:
   //   the product of one operation added to the addend of the NEXT. A steady c hides
   //   it; a varying c stream corrupts every result by one operation of skew.
   module mac_align_bad #(parameter int WIDTH = 8)(
       input  logic clk, rst,
       input  logic [WIDTH-1:0] a, b, c,
       output logic [2*WIDTH-1:0] result
   );
       logic [2*WIDTH-1:0] prod_q;
       always_ff @(posedge clk) begin
           if (rst) begin prod_q <= '0; result <= '0; end
           else begin
               prod_q <= a * b;                      // stage-1 product (one cycle old at use)
               result <= prod_q + c;                 // BUG: c is NOT delayed -> misaligned
           end
       end
   endmodule
 
   // FIXED: register c one stage (c_q) so it reaches the adder aligned with its product.
   module mac_align_good #(parameter int WIDTH = 8)(
       input  logic clk, rst,
       input  logic [WIDTH-1:0] a, b, c,
       output logic [2*WIDTH-1:0] result
   );
       logic [2*WIDTH-1:0] prod_q;
       logic [WIDTH-1:0]   c_q;                      // the missing one-stage delay
       always_ff @(posedge clk) begin
           if (rst) begin prod_q <= '0; c_q <= '0; result <= '0; end
           else begin
               prod_q <= a * b;
               c_q    <= c;                          // FIX: c travels the SAME one stage as prod
               result <= prod_q + c_q;               // now product and addend are the same op
           end
       end
   endmodule
mac_align_bug_tb.sv — a VARYING c stream exposes the skew a steady c hides
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module mac_align_bug_tb;
       localparam int WIDTH = 8, LAT = 2;
       logic clk = 1'b0, rst;
       logic [WIDTH-1:0]     a, b, c;
       logic [2*WIDTH-1:0]   result;
       logic [2*WIDTH-1:0]   ref_val [LAT+1];
       int errors = 0;
 
       // Bind to mac_align_good to PASS; to mac_align_bad to see the varying-c skew FAIL.
       mac_align_good #(.WIDTH(WIDTH)) dut (.clk(clk), .rst(rst), .a(a), .b(b), .c(c), .result(result));
       always #5 clk = ~clk;
 
       task automatic step(input [WIDTH-1:0] xa, xb, xc);
           a = xa; b = xb; c = xc;
           @(posedge clk); #1;
           for (int i = LAT; i > 0; i--) ref_val[i] = ref_val[i-1];
           ref_val[0] = xa * xb + xc;                // each result is THIS op's a*b + THIS op's c
           if (result !== ref_val[LAT])
               begin $error("skew: got=%h exp=%h (a*b from one op + c from another?)", result, ref_val[LAT]); errors++; end
       endtask
 
       initial begin
           rst = 1'b1; a = 0; b = 0; c = 0; @(posedge clk); #1; rst = 1'b0;
           @(posedge clk); #1;                        // let the pipe fill before checking
           step(8'd2, 8'd4, 8'd1  );                  // VARYING c: 1, 10, 100, 3 ...
           step(8'd3, 8'd6, 8'd10 );                  // the bug adds the WRONG cycle's c here
           step(8'd5, 8'd7, 8'd100);
           step(8'd9, 8'd2, 8'd3  );
           step(8'd1, 8'd1, 8'd1  );
           if (errors == 0) $display("PASS: c aligned with its product on every op");
           else             $display("FAIL: %0d mismatches (c misaligned by one operation)", errors);
           $finish;
       end
   endmodule

The Verilog and VHDL buggy/fixed pairs tell the identical story — the only difference between broken and correct is whether c passes through a one-stage register (c_q) before the adder.

mac_align_bug.v — BUGGY (c straight to adder) vs FIXED (c registered one stage) in Verilog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: result = prod_q + c, with c un-delayed -> a[t-1]*b[t-1] + c[t] (misaligned).
   module mac_align_bad #(parameter WIDTH = 8)(
       input  wire clk, rst,
       input  wire [WIDTH-1:0] a, b, c,
       output reg  [2*WIDTH-1:0] result
   );
       reg [2*WIDTH-1:0] prod_q;
       always @(posedge clk) begin
           if (rst) begin prod_q <= 0; result <= 0; end
           else begin
               prod_q <= a * b;
               result <= prod_q + c;                 // BUG: c not delayed
           end
       end
   endmodule
 
   // FIXED: register c one stage so it aligns with its product.
   module mac_align_good #(parameter WIDTH = 8)(
       input  wire clk, rst,
       input  wire [WIDTH-1:0] a, b, c,
       output reg  [2*WIDTH-1:0] result
   );
       reg [2*WIDTH-1:0] prod_q;
       reg [WIDTH-1:0]   c_q;
       always @(posedge clk) begin
           if (rst) begin prod_q <= 0; c_q <= 0; result <= 0; end
           else begin
               prod_q <= a * b;
               c_q    <= c;                          // FIX: the one-stage delay
               result <= prod_q + c_q;               // product and addend now same operation
           end
       end
   endmodule
mac_align_bug_tb.v — self-checking; a varying c stream catches the misalignment
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module mac_align_bug_tb;
       parameter WIDTH = 8, LAT = 2;
       reg               clk, rst;
       reg  [WIDTH-1:0]  a, b, c;
       wire [2*WIDTH-1:0] result;
       reg  [2*WIDTH-1:0] ref_val [0:LAT];
       integer           i, errors;
 
       // Bind to mac_align_good to PASS; to mac_align_bad to observe the skew.
       mac_align_good #(.WIDTH(WIDTH)) dut (.clk(clk), .rst(rst), .a(a), .b(b), .c(c), .result(result));
 
       initial clk = 1'b0;
       always #5 clk = ~clk;
 
       task step;
           input [WIDTH-1:0] xa, xb, xc;
           begin
               a = xa; b = xb; c = xc;
               @(posedge clk); #1;
               for (i = LAT; i > 0; i = i - 1) ref_val[i] = ref_val[i-1];
               ref_val[0] = xa * xb + xc;
               if (result !== ref_val[LAT]) begin
                   $display("FAIL: skew got=%h exp=%h", result, ref_val[LAT]);
                   errors = errors + 1;
               end
           end
       endtask
 
       initial begin
           errors = 0;
           rst = 1'b1; a = 0; b = 0; c = 0; @(posedge clk); #1; rst = 1'b0;
           @(posedge clk); #1;                        // fill the pipe
           step(8'd2, 8'd4, 8'd1  );                  // varying c: exposes the wrong-cycle add
           step(8'd3, 8'd6, 8'd10 );
           step(8'd5, 8'd7, 8'd100);
           step(8'd9, 8'd2, 8'd3  );
           step(8'd1, 8'd1, 8'd1  );
           if (errors == 0) $display("PASS: c aligned with its product on every op");
           else             $display("FAIL: %0d mismatches (c misaligned)", errors);
           $finish;
       end
   endmodule
mac_align_bug.vhd — BUGGY (c un-delayed) vs FIXED (c_q) in VHDL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   -- BUGGY: c added un-delayed -> product from one op + addend from the next.
   entity mac_align_bad is
       generic ( WIDTH : positive := 8 );
       port ( clk, rst : in std_logic;
              a, b, c  : in std_logic_vector(WIDTH-1 downto 0);
              result   : out std_logic_vector(2*WIDTH-1 downto 0) );
   end entity;
   architecture rtl of mac_align_bad is
       signal prod_q : unsigned(2*WIDTH-1 downto 0);
       signal res_q  : unsigned(2*WIDTH-1 downto 0);
   begin
       process (clk) begin
           if rising_edge(clk) then
               if rst = '1' then prod_q <= (others => '0'); res_q <= (others => '0');
               else
                   prod_q <= unsigned(a) * unsigned(b);
                   res_q  <= prod_q + resize(unsigned(c), 2*WIDTH);   -- BUG: c not delayed
               end if;
           end if;
       end process;
       result <= std_logic_vector(res_q);
   end architecture;
 
   -- FIXED: register c one stage (c_q) so it aligns with its product.
   entity mac_align_good is
       generic ( WIDTH : positive := 8 );
       port ( clk, rst : in std_logic;
              a, b, c  : in std_logic_vector(WIDTH-1 downto 0);
              result   : out std_logic_vector(2*WIDTH-1 downto 0) );
   end entity;
   architecture rtl of mac_align_good is
       signal prod_q : unsigned(2*WIDTH-1 downto 0);
       signal c_q    : unsigned(WIDTH-1 downto 0);
       signal res_q  : unsigned(2*WIDTH-1 downto 0);
   begin
       process (clk) begin
           if rising_edge(clk) then
               if rst = '1' then
                   prod_q <= (others => '0'); c_q <= (others => '0'); res_q <= (others => '0');
               else
                   prod_q <= unsigned(a) * unsigned(b);
                   c_q    <= unsigned(c);                              -- FIX: one-stage delay
                   res_q  <= prod_q + resize(c_q, 2*WIDTH);           -- aligned add
               end if;
           end if;
       end process;
       result <= std_logic_vector(res_q);
   end architecture;

Across all three languages the lesson is one sentence: every operand that meets at a pipeline stage must be delayed by the same number of stages as everything else it meets there — alignment is a property of the whole datapath, and a steady operand will hide a misalignment that a varying stream exposes.

5. Verification Strategy

A pipelined datapath's correctness is not "did one output look right" — it is "does the whole stream match a reference a*b + c delayed by the pipeline latency, with out_valid marking exactly the real cycles, through bubbles, a stall, and a flush." That is the one invariant, and it is what the §4 testbenches enforce.

Drive a stream of triples with in_valid, bubbles, a stall, and a flush; maintain a reference shift-register that delays each expected a*b + c (and its valid) by the pipeline latency and advances only on non-stall cycles; and assert that on every cycle out_valid equals the reference valid and, when valid, result equals the reference value.

Everything below is that invariant made checkable, and it maps directly onto the clocked testbenches in §4.

  • Latency-delayed reference self-check (the core test). Model the pipe as a LAT-deep shift register of {expected_value, expected_valid}. Each non-stall cycle, shift it and insert the new operation's a*b + c and in_valid at the head; on a stall cycle, do not advance the model (mirroring the DUT's clock-enable); on a flush cycle, clear the in-flight valids in the model. Then assert out_valid === ref_vld[LAT] every cycle and result === ref_val[LAT] whenever the reference valid is set. Because the whole stream must match, a one-operation operand skew (the §7 bug) or a mis-advanced valid fails the check. The three testbenches do exactly this (SV assert (result === ref_val[LAT]), Verilog if (result !== ref_val[LAT]) $display("FAIL…"), VHDL assert unsigned(result) = ref_val(LAT) report … severity error).
  • The result==reference invariant. Stated as a property that holds for every operation: the result emitted LAT valid-cycles after an operation entered equals that operation's a*b + c — its own a, its own b, and its own c. This is the alignment property the whole page is about; if it ever fails, an operand is misaligned. Drive a varying c stream (not a constant), because a steady c makes a misaligned c indistinguishable from an aligned one — the §7 bug is invisible under constant c and obvious under a changing one.
  • out_valid alignment invariant. Assert out_valid is low for the first LAT cycles after reset (the pipe is filling — no real results yet), high exactly on the cycles a real operation reaches the output, and low again during bubbles and after a flush. A valid that leads or lags the data it labels is the valid-bit-misalignment failure of 8.3; checking out_valid against the reference valid every cycle catches it.
  • Stall correctness. Assert a stall freezes the pipe: during a stalled cycle no result advances, and after the stall lifts the exact same stream resumes with nothing dropped or duplicated. In the testbench, hold stall high for one or more cycles mid-stream and confirm the post-stall results are precisely the pre-stall stream shifted in time. A partial freeze (some registers advance, others hold) shows up as a result that skipped or repeated an operation.
  • Flush correctness. Assert a flush drops in-flight work: after flush, the operations that were mid-pipe must not emerge as valid results (their valids were cleared), while operations entered after the flush proceed normally. Check that out_valid goes low for the flushed operations and that the data registers, though possibly still holding stale products, never surface because their valid is 0.
  • Corner cases and expected waveform. Exercise a full pipe (back-to-back valids), a fully bubbled pipe (all in_valid low), a stall coincident with a flush, and reset mid-stream (the pipe must empty — valids clear — and refill cleanly). On a correct run the waveform shows result tracking the input stream delayed by two cycles, out_valid high two cycles after each in_valid (minus flushed ones, minus stall-frozen time), and both frozen flat across any stalled cycle. A result that changes during a stall, or an out_valid that fires during the fill, is the visual signature of a stall or valid bug.

6. Common Mistakes

A stage's valid not advancing with its data. The signature valid bug. If the valid chain is shallower than the data path (for example driving out_valid from v1_q instead of v2_q, or from in_valid directly), the valid label arrives a cycle early or late and marks a fill bubble as a real result — or discards a real one. The valid chain must be exactly as deep as the data it labels: one valid register per data register, advancing on the same enable. (This is the 8.3 discipline; the DebugLab's cousin.)

An operand not registered to the same stage — misalignment. The §7 bug, and the deepest one. The adder in stage 2 sees a product that is one cycle old, so the c it adds must be one cycle old too. Feed c straight to the adder and you compute a[t-1]*b[t-1] + c[t] — a product from one operation plus an addend from the next. It passes every test that uses a constant c and fails the moment c varies. Every operand that converges at a stage must be delayed by the same number of stages as everything else arriving there (the 8.1 lesson: register operands so they meet aligned).

Partial stall — freezing some registers but not others. Stall must freeze the whole region as one unit. If the clock-enable gates the data registers but not the valid chain (or vice versa), a stalled cycle lets one part advance while the other holds, so an operation is dropped or duplicated and the stream desynchronizes. Gate every pipeline register — data and valid — with the same !stall (or clock-enable) condition, so the region pauses and resumes atomically.

Flushing the data instead of the valids (or forgetting to flush the deeper stages). Flush should clear the valid chain, not the data — clearing data is wasted logic, and garbage marked not-valid is already harmless. The real trap is flushing only the first valid register: an operation already in the second stage still emerges as valid. Flush must clear every valid register in the pipe on the flush cycle, or in-flight work at deeper stages leaks through.

Unbalanced stages — the add still on the long path. Pipelining only helps if the stages are balanced. If stage 1 is the full multiply and stage 2 is a trivial add, the multiply stage is still the critical path and you have paid a cycle of latency for little timing gain — while if a stage does too much (multiply and part of the add), the cut did not actually shorten the worst path. Place the register where it splits the delay most evenly; an unbalanced pipeline closes timing no better than the longest stage.

Under-sizing the product width. The product of two WIDTH-bit operands is 2*WIDTH bits, and a*b + c needs at least 2*WIDTH bits (with c zero- or sign-extended) to avoid overflow. Declaring result as WIDTH bits silently truncates the high half of every product — a width bug that looks like corruption. State the product width deliberately (2*WIDTH) and extend c to match before the add.

7. DebugLab

The MAC whose single-value tests all pass and whose real stream is all wrong — an un-delayed c operand

The engineering lesson: when you pipeline a datapath, correctness is not just cutting the long path — it is keeping every operand that meets at a stage aligned in time, and the operand that is easy to forget is the one that does not go through the pipelined block. The tell here is a design whose constant-operand tests all pass and whose varying-operand stream is wrong: that split is the fingerprint of an alignment error, not a broken adder or multiplier. Two habits make pipelined datapaths trustworthy, and they are the same across SystemVerilog, Verilog, and VHDL: delay every operand by the same number of stages as everything it meets (register c alongside the product it is added to), and verify with a varying-operand stream against a latency-delayed reference — never a constant-operand spot check that a misalignment slips right past.

8. Interview Q&A

9. Exercises

Design and reasoning exercises — no syntax drills. Each rehearses building and aligning a pipelined datapath.

Exercise 1 — Add a third stage

Split stage 2 into two: an add of the low half and a carry-propagate into the high half, making the pipeline three stages (latency 3). State (i) how many valid registers you now need and why, (ii) how many stages c must now be delayed to reach the adder aligned, and (iii) what stays the same about throughput. Name which Chapter-8 idea each change draws on.

Exercise 2 — Predict the misalignment

An engineer registers a and b for one stage before the multiplier (to meet input timing) but leaves c going straight into the stage-2 adder, and the multiply itself is registered. Trace, cycle by cycle, which operation's a, b, and c meet at the adder. State whether the result is correct, and if not, by how many operations c is skewed and in which direction — then give the one-line fix and the varying-operand test that distinguishes this from the §7 bug.

Exercise 3 — Make the stall correct

Given the integrated mac_pipe, an engineer gates only prod_q and result with !stall but forgets to gate the valid chain (v1_q, v2_q update every cycle). Describe precisely what happens to a stream when stall is held high for two cycles — which operations are dropped or duplicated, and what the consumer sees on out_valid. Then state the fix and the invariant a testbench should assert to catch a partial stall.

Exercise 4 — Size and balance the pipe

You must MAC a stream of 16-bit operands (a*b + c) and static timing shows the multiply is 70% of the period and the add is 20%. Give (i) the correct width of result and how you extend c before the add, (ii) whether the two-stage cut (multiply | add) is well balanced and, if not, where a better register placement or an extra stage would go, and (iii) the latency and throughput of your chosen pipeline. (Hint: a stage no shorter than the longest sub-path sets the clock.)

The Chapter-8 ideas this worked example composes (the lessons you are putting together here):

  • Why Pipeline? — Chapter 8.1; cutting the long combinational a*b + c path with a register so each stage closes timing, and why operands must be registered to meet aligned.
  • Latency vs Throughput — Chapter 8.2; the 2-cycle latency versus one-result-per-cycle throughput reading this block demonstrates.
  • Pipeline Valid-Bit Discipline — Chapter 8.3; the valid bit that marches alongside the data so the consumer knows which outputs are real — applied here as the v1_q/v2_q chain.

The datapath pieces this block is built from:

  • ALU Construction — Chapter 5.5; the adder that forms stage 2 of the pipeline.
  • Multipliers — Chapter 5; the multiply tree that forms stage 1 and is the reason the combinational path is too long.
  • Adders & Subtractors — Chapter 5; the add primitive at stage 2 and the width reasoning for a*b + c.
  • Datapath + Control Separation — Chapter 5.6; the discipline of a clean datapath, here streamed rather than sequenced.
  • FSM + Datapath — Chapter 4.6; the control/status seam a stalling consumer would drive this pipeline through.

Backward / method:

  • The RTL Design Mindset — Chapter 0.2; the Interface → State → Datapath → Control → Verification lenses this worked example walks end to end.
  • What Is an RTL Design Pattern? — Chapter 0.1; why composing patterns — not just knowing them — is the design skill this worked example rehearses.

Forward (unlock as they ship):

  • Pipeline Stalls & Flush (/rtl-design-patterns/pipeline-stalls-flush) — Chapter 8; the full treatment of the clock-enable freeze and valid-clear this page uses.
  • Pipeline Hazards (/rtl-design-patterns/pipeline-hazards) — Chapter 8; data/structural hazards that arise when pipelined operations depend on each other.
  • valid/ready Handshake (/rtl-design-patterns/valid-ready-handshake) — Chapter 9.1; the standard backpressure interface that would drive this pipeline's stall.
  • Streaming Block Case Study (/rtl-design-patterns/case-study-streaming-fifo) — Chapter 13; a full streaming datapath with FIFOs and backpressure that composes a pipe like this one.

Verilog v1 prerequisites (the grammar this datapath transcribes into):

11. Summary

  • A pipelined datapath is patterns wired together, not a new pattern. The streaming MAC is a multiply stage and an add stage (5.x / 5.5) cut apart by a pipeline register (8.1), carrying a valid bit register-for-register (8.3), with stall (a clock-enable freeze) and flush (a valid-chain clear) for backpressure. Each piece is a lesson you already had; the worked example is composing them.
  • Latency and throughput are different numbers. Two stages means latency 2 — any one result appears two cycles after its operands — but throughput one per cycle once the pipe is full, because the stages overlap different operations (8.2). You pay latency and register area to shorten the critical path and stream at full rate.
  • The valid bit shadows the data. A one-bit valid register per stage advances in lockstep with the data, so out_valid is high exactly on the cycles result is a genuine a*b + c and low while the pipe fills — the 8.3 discipline. A valid chain shallower or deeper than the data mislabels results.
  • Every operand meeting at a stage must be delayed the same number of stages. The adder sees a one-stage-old product, so c must be registered one stage (c_q) to meet its own product. Feed c straight in and each result is a[t-1]*b[t-1] + c[t] — a product from one operation plus an addend from the next. A constant c hides it; a varying c exposes it (§7).
  • Stall freezes the whole region; flush clears the valids; verify the whole stream. Stall is an all-or-nothing clock-enable so in-flight work pauses intact; flush zeroes every valid register so abandoned work stops counting. Prove it by streaming triples with bubbles, a stall, and a flush against a latency-delayed a*b + c reference and asserting result and out_valid every cycle — self-checking clocked testbenches shown in SystemVerilog, Verilog, and VHDL.