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. Fanout

Part 5 · Timing & Clocking · Intermediate

Fanout

Why high fanout is slow, and how replicated registers shorten the path when copies are preserved.

On this page

    Why this matters

    A signal that drives hundreds of flops needs a buffer tree, which adds about 0.1 ns per level: 0.5 ns at 1,024 loads. Replicating the register shortens the path, but the tool merges identical registers unless you stop it.

    After this lesson you can: estimate the delay of a high-fanout net, replicate a register, and keep the copies from being merged.

    Fanout delay

    A gate has limited drive strength. With many loads its output edge slows, so following logic switches late and buffer trees add further delay.

    levels≈⌈logF​N⌉e.g. N=256, F=4:  ⌈log4​256⌉=4

    Loads N

    Fan-out per stage F = 4

    Buffer levels

    Extra delay at ~0.1 ns per level

    16

    4

    2

    ≈ 0.2 ns

    64

    4

    3

    ≈ 0.3 ns

    256

    4

    4

    ≈ 0.4 ns

    1024

    4

    5

    ≈ 0.5 ns

    A registered signal can avoid the tree by register replication. Several identical flops each drive a fraction of the loads, trading a few extra flops for a shorter path.

    Version A drives 16 loads from one enable flop through 2 buffer levels; version B uses 4 enable copies that each drive 4 loads with no tree

    Read each panel from the enable flop on the left to the load flops on the right. The drawing uses 16 loads, the first row of the table; the code below uses N = 64.

    • In A, every load waits for two buffer stages: about 0.2 ns from the table before the logic even starts.

    • In B, each copy sees only 4 loads, so it needs no buffer tree and its path is a plain wire.

    • B costs three extra flops, and each copy still has to be kept apart from the others by the tool (next section).

    Version B slices the data as q[c*PER +: PER] with PER = N / COPIES, so it requires N to be a multiple of COPIES. If it is not, integer division truncates PER and the top bits of q are never driven (they stay x); the module therefore opens with an elaboration-time $error guard that stops the build instead.

    Keeping replicated registers

    Warning

    Identical registers get merged. In a Yosys run, the four enable copies in version B collapsed into a single flop even with a (* keep *) attribute on the net, so version A and version B ended up with the same one enable flop.

    Commercial tools do the same by default: they see logically identical registers and merge them. To keep the copies you must say so: a preserve-hierarchy boundary, or the tool's “don't merge / preserve register” attribute.

    Diagram
    yosys result for sel_regs_b with (* keep *) on the enable net:     1 enable flop  (copies merged)
    yosys result with a (* keep_hierarchy *) one-flop module per copy:      4 enable flops (copies kept)

    The listing below uses the second approach: the en_reg module with (* keep_hierarchy *) on each instance. Synthesis keeps four instances, each with its own flop, and the testbench confirms output bit-identical to the single-enable version over 5,000 random cycles.

    Tip

    Replicate at the source: copy the register, not the logic before it. The enable here is registered, so each copy has its own full-cycle path.

    If you replicate combinational logic, the copies also load the upstream signals.

    Common pitfall

    Don't duplicate blindly. Each copy adds area, power and a load on its own driver.

    Replicate only the nets that show up as high-fanout contributors in the timing report, and prefer 2 to 4 copies, not 64.

    Check yourself A reset signal fans out to 3,000 flops. Does equal RTL reset value make replicated reset flops safe?

    No. Equal values do not establish physical release alignment or recovery/removal timing. Use the approved shared release synchronizer and reset distribution tree for each destination domain; independent regional synchronizers may release on different cycles. Any registered branches need an explicit release/latency contract and implementation approval.

    Reset fanout closure requires reset-tree synthesis/placement review, branch skew and min-pulse checks, recovery/removal analysis at all sinks and corners, and CDC/RDC review of interacting regions. The enable replication experiment below supplies no reset signoff evidence. See Reset tree planning for shared-synchronizer and branch methodology.

    Testbench

    Both enable styles plus a testbench that proves they match over 5,000 random cycles.

    sel_regs_a.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
    // Version A: one enable register drives all N loads.
    module sel_regs_a #(parameter int N = 64) (
      input  logic clk, rst_n, en_in,
      input  logic [N-1:0] d,
      output logic [N-1:0] q
    );
      logic en;
      always_ff @(posedge clk or negedge rst_n) if (!rst_n) en <= 1'b0; else en <= en_in;
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n)  q <= '0;
        else if (en) q <= d;                          // 'en' fans out to N flip-flops
    endmodule
    
    // A one-flop module: instantiating it several times with keep_hierarchy stops synthesis from merging the copies.
    module en_reg (input logic clk, rst_n, d, output logic q);
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) q <= 1'b0;
        else        q <= d;
    endmodule
    
    // Version B: the enable register is replicated, each copy drives N/COPIES loads.
    module sel_regs_b #(parameter int N = 64, parameter int COPIES = 4) (
      input  logic clk, rst_n, en_in,
      input  logic [N-1:0] d,
      output logic [N-1:0] q
    );
      localparam int PER = N / COPIES;
      if (N % COPIES != 0) $error("N must be a multiple of COPIES");   // elaboration-time guard: otherwise the top bits stay undriven
      logic [COPIES-1:0] en;
      for (genvar c = 0; c < COPIES; c++) begin : g_copy
        (* keep_hierarchy *) en_reg u_en (.clk, .rst_n, .d(en_in), .q(en[c]));   // one real flop per copy
        always_ff @(posedge clk or negedge rst_n)
          if (!rst_n)      q[c*PER +: PER] <= '0;
          else if (en[c])  q[c*PER +: PER] <= d[c*PER +: PER];                    // each copy drives only PER flops
      end
    endmodule
    
    module tb_fan;
      timeunit 1ns; timeprecision 1ps;
      localparam int N = 64;
      logic clk = 0, rst_n = 1, en_in = 0;
      logic [N-1:0] d = '0, qa, qb;
      sel_regs_a #(N)    a (.clk, .rst_n, .en_in, .d, .q(qa));
      sel_regs_b #(N, 4) b (.clk, .rst_n, .en_in, .d, .q(qb));
      always #5 clk = ~clk;
      int errors = 0;
      initial begin
        #1 rst_n = 0; @(posedge clk); #1 rst_n = 1;
        for (int i = 0; i < 5000; i++) begin
          @(negedge clk);
          en_in = $urandom_range(0, 1);
          d = {$urandom, $urandom};
          @(posedge clk); #1;
          if (qa !== qb) errors++;
        end
        if (errors == 0) $display("PASS: replicating the enable register changes nothing functionally (5000 cycles, 64-bit data)");
        else $fatal(1, "FAIL: %0d mismatches", errors);
        $finish;
      end
    endmodule

    Remember

    • Buffer levels grow with log of the load count: 2 levels at 16 loads, 5 at 1,024.

    • Replicate the register, not the logic before it. Use 2 to 4 copies, only on nets the timing report shows as high fanout.

    • Identical registers are merged. The copies survive only with a preserve-hierarchy boundary or the tool's do-not-merge attribute.

    Rule of thumb: replicate at the source, and check the synthesis report to see that the copies are still there.

    How this was checked

    Yosys results: sel_regs_b with (* keep ) on the enable net gives 1 enable flop (copies merged). With a ( keep_hierarchy *) one-flop module per copy it gives 4 enable flops. The two versions match over 5,000 random cycles.

    The enable replication experiment supplies no reset signoff evidence. Reset fanout needs reset-tree review, recovery and removal analysis at all sinks and corners, and CDC and RDC review.

    Keep going

    • Interview: run a mock interview on this topic

    Related topics

    • Patterns

    Previous

    142 / 350

    Next: Patterns

    Up next

    Up nextPatterns

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • RTL Hooks
    • Checklist
    • Cutting Logic Depth: a Timing-Closure Story