Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • RTL Foundations36
  • Combinational Design25
  • Sequential Design27
  • FSM & Control27
  • Timing & Clocking39
    • STA Foundations
    • Setup, Hold and Slack
    • Skew and Variation
    • Timing Exceptions
    • Clock Gating
    • RTL for Timing
      • Practice
      • Fanout
      • Patterns
      • RTL Hooks
      • Checklist
      • Cutting Logic Depth: a Timing-Closure Story
      • Mini Project: Close Timing on a FIR Filter
    • Clock Architecture
  • Reset & CDC34
  • Memory & Buses57
  • Advanced RTL28
  • Senior & Interview76
  • Glossary and Notation
Loading lesson

‹RTL for Timing
  1. Topics
  2. RTL Design
  3. Timing & Clocking
  4. RTL for Timing
  5. Cutting Logic Depth: a Timing-Closure Story

Part 5 · Timing & Clocking · Intermediate

Cutting Logic Depth: a Timing-Closure Story

One block taken from 119 logic levels to 36 in four measured steps: restructure, cut, then change the feedback loop that pipelining cannot touch.

On this page

    Why this matters

    A block that misses timing by three times its depth budget is not fixed by one trick. Restructuring, a register cut and a loop change each solve a different part, and the third step fails if you try to pipeline the loop.

    After this lesson you can: measure logic depth with a synthesis run, choose between restructuring, a register cut and a loop change, count the flops and latency each costs, and explain why a feedback loop cannot be pipelined.

    A block that needs three times its budget

    The peak-sum block adds the largest of eight 16-bit lanes to a running 24-bit total, every cycle. Its first version has a longest path of 119 logic levels. The budget for this example is 38 levels, so the path is more than three times too long.

    Four steps bring it inside the budget. Each one was measured, each one costs something, and the last one exists because the third ran into a feedback loop.

    Longest path in logic levels and flop count for each step: 119, 73, 39 and 36 levels against a budget of 38; 152, 152, 168 and 185 flops

    Read the top chart against the dashed line; read the bottom chart as the bill for each step.

    • Step 2 removes 46 levels and costs no flops: it is the same function in a different shape.

    • Step 3 halves the path again but adds 16 flops and one cycle of latency.

    • Step 4 gains only 3 levels, yet it is the one that crosses the line. It adds 17 flops and another cycle.

    How depth is measured here

    There is no timing library in this experiment, so the measure is logic levels: the number of generic gates (AND, OR, XOR, MUX, NOT) on the longest path between two flops. Each version is synthesised and the longest path is reported by Yosys:

    Diagram
    $ yosys -p "read_verilog -sv design.sv; synth -flatten -noabc -top peak_v1; abc -g AND,OR,XOR,MUX; opt_clean; stat; ltp -noff"
    ...
         1122 cells
          360   $_AND_
          152   $_DFF_PN0_
    ...
    Longest topological path in peak_v1 (length=119):
        0: \x_q [26]
        1: $abc$2518$new_n220 (via $abc$2518$auto$blifparse.cc:397:parse_blif$2562)
    ...
      119: $auto$alumacc.cc:548:replace_alu$143.Y [23] (via $abc$2518$auto$blifparse.cc:397:parse_blif$3462)
       ff: \acc [23] (via $auto$ff.cc:337:slice$505)
    • length=119 is the depth: 119 gates in series.

    • 0: \x_q [26] is where the path starts: bit 26 of the input register, which is lane 1.

    • ff: \acc [23] is where it ends: the top bit of the total. The path runs through the compare chain and then the whole carry of the adder.

    • $_DFF_PN0_ counts the flops: 128 for the lanes plus 24 for the total.

    Logic levels are not nanoseconds. To connect them to timing, this lesson uses one assumption throughout: 0.05 ns per generic level, including wire (an assumption for this example, not a library figure). The clock is 2.5 ns, with 0.4 ns clock-to-Q and 0.2 ns setup, which leaves 1.9 ns, or 38 levels.

    Step

    Levels

    Logic at 0.05 ns/level

    Setup slack at 2.5 ns

    1 chain

    119

    5.95 ns

    2.5 − 0.4 − 0.2 − 5.95 = −4.05 ns

    2 tree

    73

    3.65 ns

    −1.75 ns

    3 cut

    39

    1.95 ns

    −0.05 ns

    4 split loop

    36

    1.80 ns

    +0.10 ns

    Slack calculator

    Loading slack calculator…

    Set to step 3: 39 levels at the assumed 0.05 ns each is 1.95 ns of logic, 0.05 ns too long. Type 1.80 for step 4, or 5.95 for step 1.

    Step 1 and 2: a tree instead of a chain

    The baseline finds the maximum the way a loop in software does: compare lane 1 with lane 0, then lane 2 with that result, and so on. Each compare waits for the one before it.

    design.sv
    1
    2
    3
    4
    // snippet: step 1, from peak_v1 below
    for (genvar i = 1; i < 8; i++) begin : g_chain
      assign c[i] = (x_q[i*16 +: 16] > c[i-1]) ? x_q[i*16 +: 16] : c[i-1];
    end

    The tree compares pairs instead: four compares at once, then two, then one. Maximum is associative, so the result is the same; only the shape changes.

    design.sv
    1
    2
    3
    4
    5
    // snippet: step 2, the max8_tree module below
    for (genvar i = 0; i < 4; i++) begin : g_l1   // level 1: four pairs
      assign l1[i] = (x[(2*i+1)*16 +: 16] > x[(2*i)*16 +: 16]) ? x[(2*i+1)*16 +: 16] : x[(2*i)*16 +: 16];
    end
    // level 2: two pairs, level 3: one
    Seven compare-selects in a chain feeding the adder, against a three-level tree whose result is registered in m_q before the accumulator

    Read each drawing from the lanes on the left to the total on the right.

    • The chain puts seven compare-selects in series; the tree puts three.

    • Measured, the depth falls from 119 to 73 levels. The tree has 1046 gates against 970 for the chain (cells minus flops): a little more logic for much less depth.

    • The adder is still in the same cycle, so the path in step 2 is the tree plus the full carry of a 24-bit add.

    Step 3: a register at the right cut

    The natural cut is between the tree and the adder: one 16-bit register, m_q. The tree and the adder now sit in different cycles.

    design.sv
    // snippet: step 3, from peak_v3 below
    else begin x_q <= x;  m_q <= m;  acc <= acc + m_q; end
    Diagram
    Longest topological path in peak_v3 (length=39):
        0: \acc [1]
    ...
       ff: \acc [23] (via $auto$ff.cc:337:slice$399)

    The path now starts and ends at acc. It is the accumulator loop: the total goes through the adder and back into its own register. The tree stage, measured on its own, is 35 levels. The cut cost 16 flops and one cycle, and the block still misses by one level.

    Adding another register does not help here. A register can split the tree stage, but the loop is acc <= acc + m_q: one adder and one register, and any register added inside it changes what the block computes (see Common mistakes). Retiming cannot move a register out of the loop either; see retiming mechanism.

    Step 4: change the loop, not the pipeline

    The fix is to make the loop itself shorter. Split the 24-bit total into two 12-bit halves. The low half adds the low 12 bits of m_q and puts its carry into a flop, c_q. The high half adds that carry one cycle later, together with the high bits of m_q, which wait one cycle in mh_q.

    design.sv
    1
    2
    3
    4
    5
    6
    // snippet: step 4, from peak_v4 below
    {c_q, lo} <= lo + m_q[11:0];               // low loop: carry out goes to a flop
    mh_q <= m_q[15:12];                        // high bits of m wait one cycle for that carry
    hi <= hi + 12'(mh_q) + 12'(c_q);           // high loop: carry in from the flop
    lo_q <= lo;                                // line the low half up with the high half
    assign acc = {hi, lo_q};
    • Each loop still has one register. lo feeds lo, and hi feeds hi, so neither total changes meaning. The carry flop sits between the loops, not inside one.

    • Every carry is counted once, one cycle late. That is why the high half and lo_q show the total one cycle after step 3.

    • Measured: the split loop is 23 levels deep. The longest path is now the tree stage at 36, inside the budget.

    • Cost: 17 flops (1 carry, 4 for mh_q, 12 for lo_q) and one more cycle of latency.

    Step

    Levels

    Where the path is

    Flops

    Gates

    Latency added

    1 chain

    119

    x_q to acc

    152

    970

    0

    2 tree

    73

    x_q to acc

    152

    1046

    0

    3 cut

    39

    acc loop

    168

    1025

    1

    4 split loop

    36

    x_q to m_q

    185

    1020

    2

    The bench below runs all four versions on 4000 cycles of directed and random lanes, and checks step 3 one cycle late and step 4 two cycles late against step 1.

    The order to try fixes

    Flow: read the path. If it is inside a feedback loop, change the loop by splitting it or adding lookahead. Otherwise restructure; if still over budget, add a register at a stage boundary and retime to balance; if not, re-measure every stage.
    Restructure first because it costs no latency; add registers only outside loops; a loop needs a different structure.

    Read it top to bottom; each step costs more than the one above it. Restructuring is free in latency, a register is not, and a loop change also changes how the result is presented. The patterns lesson has more restructurings, and the retiming decision guide covers when to let the tool move registers.

    Common mistakes

    Common pitfall

    Do not pipeline a feedback loop. peak_bad below adds a register inside the accumulator loop: s_q <= acc + m_q; acc <= s_q;. Yosys still reports 39 levels, now ending at s_q, because the adder is still one stage. And the function is gone: two registers around the loop keep two interleaved totals, and the bench found the output different from the correctly delayed total on 3998 of 4000 cycles.

    A loop's depth is cut only by changing the loop: split it with a registered carry as in step 4, use lookahead, or keep a redundant (carry-save) total.

    Warning

    Limits: logic levels count generic two-input gates. They ignore wire delay, fanout, cell drive strength and the different delays of real cells, and ABC's mapping moved the tree stage between 35 and 36 levels depending on the surrounding logic. The 0.05 ns per level is an assumption chosen for this example. Confirm the choice with a timing report on the target library before you pay for a cut.

    Check yourself After step 3, someone proposes a second pipeline register inside the tree. How many levels does the longest path lose?

    None. The longest path after step 3 is the accumulator loop at 39 levels; the tree stage is already 35. Cutting the tree only shortens a path that is not critical, and it costs flops and latency.

    Check yourself Step 4 splits a 24-bit loop into two 12-bit loops with a registered carry. Why does the high half need mh_q, the high bits of m delayed one cycle?

    The carry from the low half of value k reaches the high half one cycle late. The high bits of value k must arrive in that same cycle, or the high half would add value k's bits with value k-1's carry. Both are counted once, so the total is right, one cycle later.

    Testbench

    All versions plus a self-checking bench. To repeat the depth measurements, save the modules before tb_peak as design.sv and run the Yosys command above with each top.

    max8_tree.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    // Peak-sum block: each cycle, add the largest of eight 16-bit lanes to a 24-bit total.
    // Every version registers its inputs in x_q, so each measured path starts and ends at a flop.
    
    // Shared by steps 2 to 4: a balanced tree of compare-selects, 3 comparators deep.
    module max8_tree (input logic [8*16-1:0] x, output logic [15:0] m);
      logic [3:0][15:0] l1;
      logic [1:0][15:0] l2;
      for (genvar i = 0; i < 4; i++) begin : g_l1   // level 1: four pairs
        assign l1[i] = (x[(2*i+1)*16 +: 16] > x[(2*i)*16 +: 16]) ? x[(2*i+1)*16 +: 16] : x[(2*i)*16 +: 16];
      end
      for (genvar i = 0; i < 2; i++) begin : g_l2   // level 2: two pairs
        assign l2[i] = (l1[2*i+1] > l1[2*i]) ? l1[2*i+1] : l1[2*i];
      end
      assign m = (l2[1] > l2[0]) ? l2[1] : l2[0];   // level 3
    endmodule
    
    // Step 1 (baseline): a chain of 7 compare-selects, then the accumulator add, in one cycle.
    module peak_v1 (input logic clk, rst_n, input logic [8*16-1:0] x, output logic [23:0] acc);
      logic [8*16-1:0] x_q;
      logic [7:0][15:0] c;                          // c[i] = largest of lanes 0 to i
      assign c[0] = x_q[15:0];
      for (genvar i = 1; i < 8; i++) begin : g_chain
        assign c[i] = (x_q[i*16 +: 16] > c[i-1]) ? x_q[i*16 +: 16] : c[i-1];
      end
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) begin x_q <= '0; acc <= '0; end
        else        begin x_q <= x;  acc <= acc + c[7]; end
    endmodule
    
    // Step 2: the same function with the tree. Same latency, same flops.
    module peak_v2 (input logic clk, rst_n, input logic [8*16-1:0] x, output logic [23:0] acc);
      logic [8*16-1:0] x_q;
      logic [15:0] m;
      max8_tree u_max (.x(x_q), .m);
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) begin x_q <= '0; acc <= '0; end
        else        begin x_q <= x;  acc <= acc + m; end
    endmodule
    
    // Step 3: a register between the tree and the accumulator. One more cycle of latency.
    module peak_v3 (input logic clk, rst_n, input logic [8*16-1:0] x, output logic [23:0] acc);
      logic [8*16-1:0] x_q;
      logic [15:0] m, m_q;
      max8_tree u_max (.x(x_q), .m);
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) begin x_q <= '0; m_q <= '0; acc <= '0; end
        else        begin x_q <= x;  m_q <= m;  acc <= acc + m_q; end
    endmodule
    
    // Step 4: split the accumulator loop into two 12-bit loops joined by a registered carry.
    // Each loop still has exactly one register; the total appears one cycle later than in step 3.
    module peak_v4 (input logic clk, rst_n, input logic [8*16-1:0] x, output logic [23:0] acc);
      logic [8*16-1:0] x_q;
      logic [15:0] m, m_q;
      logic [11:0] lo, lo_q, hi;
      logic [3:0]  mh_q;
      logic        c_q;
      max8_tree u_max (.x(x_q), .m);
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) begin
          x_q <= '0; m_q <= '0; lo <= '0; c_q <= 1'b0; mh_q <= '0; hi <= '0; lo_q <= '0;
        end else begin
          x_q <= x;
          m_q <= m;
          {c_q, lo} <= lo + m_q[11:0];   // low loop: carry out goes to a flop
          mh_q <= m_q[15:12];            // high bits of m wait one cycle for that carry
          hi <= hi + 12'(mh_q) + 12'(c_q);   // high loop: carry in from the flop
          lo_q <= lo;                    // delay the low half to line up with the high half
        end
      assign acc = {hi, lo_q};
    endmodule
    
    // The mistake: a "pipeline" register inside the accumulator loop. Two registers now sit
    // around the loop, so it keeps two interleaved totals instead of one.
    module peak_bad (input logic clk, rst_n, input logic [8*16-1:0] x, output logic [23:0] acc);
      logic [8*16-1:0] x_q;
      logic [15:0] m, m_q;
      logic [23:0] s_q;
      max8_tree u_max (.x(x_q), .m);
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) begin x_q <= '0; m_q <= '0; s_q <= '0; acc <= '0; end
        else        begin x_q <= x;  m_q <= m;  s_q <= acc + m_q; acc <= s_q; end
    endmodule
    

    Remember

    • Restructure first: a tree instead of a chain cut 119 levels to 73 with no flops and no latency.

    • A register only helps outside a loop. Once the loop is the longest path, extra registers add latency and change nothing, or change the function.

    • Shorten a loop by changing it: split it with a registered carry, use lookahead or a redundant total, and align the result.

    Rule of thumb: find where the longest path starts and ends before choosing a fix; if it starts and ends at the same register, change the loop.

    How this was checked

    Tools: Yosys 0.69+post (git sha1 143eb14f9), Icarus Verilog 13.0 and Verilator 5.048, run on 2026-10-17. Depth: yosys -p "read_verilog -sv design.sv; synth -flatten -noabc -top <top>; abc -g AND,OR,XOR,MUX; opt_clean; stat; ltp -noff" on the design modules of the lesson block (checked identical to the measured file).

    Results (longest path levels / cells / flops): peak_v1 119 / 1122 / 152, path x_q to acc; peak_v2 73 / 1198 / 152; peak_v3 39 / 1193 / 168, path acc to acc; peak_v4 36 / 1205 / 185, path x_q to m_q; peak_bad 39 / 1212 / 192, path acc to s_q. Separate measurement tops: tree stage alone (x_q to m_q) 35 levels; the split loop alone 23 levels. Gate counts in the lesson are cells minus flops.

    Bench: iverilog -g2012 design.sv tb.sv; vvp -n, and verilator --binary: PASS in both, 4000 cycles (directed lanes, ties, all-ones to force wrap, then random), steps 1-4 equal after aligning steps 3 and 4 by one and two cycles; peak_bad differs from the aligned total on 3998 cycles.

    Scope and limits: generic two-input gates mapped by ABC, no library, no wire load, fanout or drive; the 0.05 ns per level, 2.5 ns clock, 0.4 ns clock-to-Q and 0.2 ns setup are assumptions for the example, not measured delays. ABC put the same tree stage at 35 or 36 levels depending on context.

    Keep going

    • Practice: Pipelined arbiter for timing closure

    • Code lab: AI pooling max

    • Code lab: Pipeline register stage

    • Interview: run a mock interview on this topic

    Related topics

    • Mini project: close timing on a FIR filter

    • Why RTL architecture dominates

    • Throughput and latency

    Previous

    146 / 350

    Next: Mini Project: Close Timing on a FIR Filter

    Up next

    Up nextMini Project: Close Timing on a FIR Filter

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Practice
    • Fanout
    • Patterns