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

  • How to Use This Course
  • Reading Paths
  • Concept Map
  • How to Read the Diagrams
  • Part 0: What Is a Protocol?10
  • Part 1: Timing and Clocking11
  • Part 2: Encoding and Framing12
  • Part 3: Handshakes13
  • Part 4: Flow Control17
    • Why Flow Control
    • On/Off Flow Control
    • Credit-Based Flow Control
    • Credit Initialisation
    • The Credit Loop and Buffer Sizing
    • Credit Classes and Pools
    • Credit Return Latency and Batching
    • Credit Bugs
    • Window Flow Control
    • Rate-Based Shaping
    • Hop-by-Hop or End-to-End
    • Lossless or Lossy
    • Switching Modes
    • Head-of-Line Blocking
    • Choosing Flow Control
    • Protocols Compared: Flow Control
    • Drills: Flow Control
  • Part 5: Transactions19
  • Part 6: Concurrency and Performance17
  • Part 7: Ordering13
  • Part 8: Sharing and Arbitration12
  • Part 9: Deadlock and Liveness13
  • Part 10: Reliability and Recovery20
  • Part 11: Bring-Up and Link Management10
  • Part 12: Power8
  • Part 13: Shared State and Coherence9
  • Part 14: Security and Isolation7
  • Part 15: Specifying and Verifying11
  • Part 16: Protocols Dissected17
  • Part 17: Capstones5
  • Part 18: Interview Preparation6
Loading lesson

‹Part 4: Flow Control
  1. Topics
  2. First Principles of Hardware Protocols
  3. Part 4: Flow Control
  4. The Credit Loop and Buffer Sizing

Part 4 · Flow Control · Senior (S)

The Credit Loop and Buffer Sizing

Why a credit-controlled link needs a receiver buffer at least as large as bandwidth times the credit round trip, and how to size it.

On this page

    Note

    Level: Senior (S) · Answers: How much may I send before I hear back?

    Before this: Part 4 overview, The Valid/Ready Handshake

    Two blocks sit far apart on a die, so the route between them has four pipeline stages in each direction. The link uses credit flow control and the receiver has a four-flit buffer. The receiver drains a flit every cycle it has one; it never holds anything back. Yet the measured throughput is 40% of one flit per cycle, and nobody can find a stall in the receiver.

    Nothing is broken. The sender is waiting for credits that are still travelling. How big must the receiver's buffer be for a credit-controlled link to run at full speed?

    The obvious answer, and why it breaks

    The usual first answer is that the buffer only has to absorb bursts: if the receiver drains as fast as the sender sends, a buffer of a few flits is plenty, because nothing ever piles up in it.

    That reasoning treats the buffer as a place where data waits. With credits, the buffer size is also the number of credits in circulation, and a credit is useless while it travels. After the sender spends a credit, that credit has to ride forward with its flit, wait for the receiver to free the slot, and ride back before it can be spent again. If the sender owns four credits and each one takes ten cycles to come home, it can send at most four flits every ten cycles, however fast the receiver drains. The buffer is not too small for the data; it is too small for the round trip.

    The idea: credits circulate, and the loop has a length

    Follow one credit around the loop. Count cycles from the moment it is spent to the moment it can be spent again; call that the credit round-trip time, RTT. Every credit is either at the sender, attached to a flit in flight, sitting in the receiver's buffer as an occupied slot, or travelling back. The total never changes: it is always B, the buffer size. That is the credit invariant, and it is the first thing to assert in any credit design.

    Because each credit can be used at most once per RTT, the sender can send at most B flits per RTT:

    throughput=min(1,RTTB​) flits per cycle

    For full throughput at one flit per cycle, the buffer must cover the round trip: B ≥ RTT. In units of bytes and seconds this is the bandwidth-delay product, a result as old as networking:

    Bbytes​≥bandwidth×RTT
    The opening example drawn on the canonical figure. With four stages each way the round trip is ten cycles, so four credits allow at most four flits per ten cycles.

    Count the round trip exactly

    RTT is longer than the wires alone, and the extra cycles are where sizing mistakes hide. In the reference model used below, a credit spent in cycle t goes through these steps:

    Step

    Cycles

    Where the credit is

    Flit travels forward

    L

    In the forward pipeline, attached to its flit

    Flit waits one cycle in the buffer before the receiver drains it

    1

    As an occupied slot

    Credit travels back

    L

    In the return pipeline

    Sender's counter updates on the next edge

    1

    Arriving at the sender

    Total

    2L + 2

    Usable again in cycle t + 2L + 2

    With L = 4 that is 10 cycles, so the opening example's four credits give 4 / 10 = 40%. In a real design, add every cycle of logic the credit passes through: arbitration for the return channel, clock-domain crossings, the receiver's own pipeline before a slot is truly free. Size from the worst-case round trip, not the typical one, because the loop runs at the speed of its slowest lap.

    Build it: a model, then the circuit

    First, a cycle model. It sends a flit whenever the sender holds a credit, moves flits and credits through L-stage pipelines, drains one flit per cycle at the receiver, asserts the credit invariant every cycle, and measures the steady-state rate for three link lengths and ten buffer sizes.

    example.py
    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
    """Cycle model of credit-based flow control over a pipelined link.
    
    Each cycle the sender sends one flit if it holds a credit. A flit takes L cycles
    to reach the receiver's buffer. The receiver removes one flit per drain slot and
    returns one credit, which takes L cycles to reach the sender. The model checks
    the credit invariant every cycle and measures steady-state throughput.
    """
    from collections import deque
    
    
    def simulate(L, B, cycles=6000, drain_every=1):
        credits = B                       # credits held by the sender
        fwd = deque([0] * L)              # forward pipeline: 1 = a flit in that stage
        back = deque([0] * L)             # return pipeline: 1 = a credit in that stage
        buf = 0                           # flits waiting in the receiver buffer
        sent_late = 0
        for t in range(cycles):
            arrive = fwd[-1]              # flit reaching the receiver this cycle
            crd_in = back[-1]             # credit reaching the sender this cycle
            send = 1 if credits > 0 else 0
            pop = 1 if (buf > 0 and t % drain_every == 0) else 0
            # rising edge: every register updates at once
            credits += crd_in - send
            buf += arrive - pop
            fwd.rotate(1); fwd[0] = send
            back.rotate(1); back[0] = pop
            # invariant: every credit is somewhere, and the buffer never overflows
            assert credits + sum(fwd) + buf + sum(back) == B
            assert 0 <= buf <= B
            if t >= cycles // 2:
                sent_late += send
        return sent_late / (cycles - cycles // 2)
    
    
    BUFFERS = [1, 2, 4, 6, 8, 10, 12, 14, 16, 18]
    print("throughput (flits per cycle) by buffer depth B; RTT = 2L + 2")
    print("   B: " + " ".join(f"{b:>5}" for b in BUFFERS))
    for L in (1, 4, 8):
        rtt = 2 * L + 2
        row = [simulate(L, b) for b in BUFFERS]
        for b, thr in zip(BUFFERS, row):
            assert abs(thr - min(1.0, b / rtt)) < 0.002, (L, b, thr)
        print(f"L={L:<2} " + " ".join(f"{x:5.3f}" for x in row) + f"   (RTT {rtt})")
    
    # A receiver that drains only every other cycle caps throughput at 0.5.
    half = [simulate(4, b, drain_every=2) for b in BUFFERS]
    print("L=4, drain every 2nd cycle: " + " ".join(f"{x:5.3f}" for x in half))
    print("PASS: measured throughput equals min(1, B / RTT) for every case")

    It prints:

    Diagram
    throughput (flits per cycle) by buffer depth B; RTT = 2L + 2
       B:     1     2     4     6     8    10    12    14    16    18
    L=1  0.250 0.500 1.000 1.000 1.000 1.000 1.000 1.000 1.000 1.000   (RTT 4)
    L=4  0.100 0.200 0.400 0.600 0.800 1.000 1.000 1.000 1.000 1.000   (RTT 10)
    L=8  0.056 0.111 0.223 0.334 0.445 0.555 0.666 0.777 0.889 1.000   (RTT 18)
    L=4, drain every 2nd cycle: 0.100 0.200 0.400 0.500 0.500 0.500 0.500 0.500 0.500 0.500
    PASS: measured throughput equals min(1, B / RTT) for every case
    The model's numbers, plotted. Each line rises in a straight line, B / RTT, until B reaches the round trip, then stays flat at 100%. A longer link needs a deeper buffer for the same rate.

    Every line is a straight ramp of slope 1/RTT that flattens at 100% exactly when B reaches the round trip. The last line of the output shows a receiver that drains only every other cycle: throughput is capped at 50%, and it reaches that cap at B = 6 (B = 4 gives only 40%). A receiver that drains at rate r needs only about r × RTT credits, here 0.5 × 10 = 5. Credits beyond that buy nothing.

    Now the circuit. The testbench below builds the same loop in SystemVerilog with L = 2 and B = 3, so RTT = 6, and checks the invariant and the steady-state rate.

    tb.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
    // Credit flow control over a pipelined link: L = 2 stages each way, B = 3 receiver
    // slots. The credit round trip is RTT = 2L + 2 = 6 cycles, so the sender can send
    // only B = 3 flits per 6 cycles: 50% throughput, even though the receiver drains
    // a flit every cycle it has one. The checker confirms the credit invariant and the
    // steady-state rate.
    module tb;
      localparam int L = 2, B = 3, RTT = 2 * L + 2;
      reg clk = 0;
      always #5 clk = ~clk;
      reg rst_n = 0;
      initial begin
        $dumpfile("wave.vcd");
        $dumpvars(0, tb);
        #12 rst_n = 1;
      end
    
      // Sender: spends one credit per flit.
      reg  [3:0] credits = B;
      wire       send = (credits != 0);
    
      // Forward and return pipelines (one register per stage).
      reg  [L-1:0] fwd  = 0;
      reg  [L-1:0] back = 0;
      wire rx_vld = fwd[L-1];          // flit reaching the receiver this cycle
      wire crd_in = back[L-1];         // credit reaching the sender this cycle
    
      // Receiver: a B-slot buffer, drained one flit per cycle; each drained slot
      // sends one credit back.
      reg  [3:0] occ = 0;
      wire       pop = (occ != 0);
    
      always @(posedge clk) begin
        if (!rst_n) begin
          credits <= B; fwd <= 0; back <= 0; occ <= 0;
        end else begin
          credits <= credits + crd_in - send;
          fwd     <= {fwd[L-2:0], send};
          back    <= {back[L-2:0], pop};
          occ     <= occ + rx_vld - pop;
        end
      end
    
      // Checker
      integer t = 0, sends = 0, errors = 0;
      function automatic int ones(input [L-1:0] v);
        ones = 0;
        for (int i = 0; i < L; i++) ones += v[i];
      endfunction
      always @(posedge clk) if (rst_n) begin
        if (credits + ones(fwd) + occ + ones(back) != B) begin
          $display("ERROR: credit invariant broken at cycle %0d", t);
          errors++;
        end
        if (occ > B) begin
          $display("ERROR: receiver buffer overflow at cycle %0d", t);
          errors++;
        end
        if (t >= 2 * RTT && t < 6 * RTT) sends += send;  // four round trips in steady state
        t++;
      end
    
      initial begin
        #400;
        if (errors == 0 && sends == 4 * B)
          $display("PASS: %0d flits in %0d cycles (B = %0d per RTT of %0d cycles), no overflow", sends, 4 * RTT, B, RTT);
        else
          $display("FAIL: %0d errors, %0d flits in %0d cycles", errors, sends, 4 * RTT);
        $finish;
      end
    endmodule

    Watch it run

    How to read it, slot by slot:

    • Slots 0–2: the sender spends its three credits (credits 3, 2, 1; send high).

    • Slots 3–5: credits is 0 and send is low. The receiver is not stalled: it drains a flit in each of these slots (pop high). The credits it frees are on the return path.

    • Slot 5: the first credit arrives (crd_in). The counter updates on the next edge, and in slot 6 the sender sends again. The credit spent in slot 0 was reused in slot 6: one round trip of six cycles.

    • From slot 6: the pattern repeats, three flits on and three off. Throughput is 3 / 6 = 50%, as the formula says. With B = 6 the sender would never run dry.

    Try it

    Change the pipeline stages, the credits, the batch size and the drain rate, then step through the loop. The plot underneath measures steady throughput for every buffer size from 1 to 32, using the same model as the listing above.

    Credit loop simulator

    Loading credit loop simulator…

    Starts at the opening example: four stages each way and four credits, so 40%. Raise the credits to 10 to reach full rate, then try a batch size above 1.

    Worked numbers: from cycles to bytes

    Real links are sized in bytes and nanoseconds. With illustrative numbers: a link carrying 16 GB/s whose credit round trip (wire, logic, return arbitration and update) is 500 ns needs

    Diagram
    buffer >= bandwidth x RTT
           = 16 GB/s x 500 ns
           = 16e9 B/s x 500e-9 s
           = 8,000 bytes
    In 16-byte data credits (the PCIe data-credit unit): 8,000 / 16 = 500 data credits

    Halve the round trip and the buffer halves. Double the bandwidth and it doubles. This is why buffer area grows with both link speed and distance, and why every cycle shaved off the credit return path is worth real silicon.

    The sizing rule is a case of Little's law: the average number of items in a system equals their throughput times the time each spends there. Credits in circulation are the items, the round trip is the time, so full rate needs B ≥ 1 × RTT. The same law limits any requester with a cap on outstanding requests (Part 6): to keep a memory with 40 cycles of latency busy at half a request per cycle, it needs 20 requests in flight.

    Little's law: outstanding requests

    Loading little's law tool…

    A requester allowed only 8 outstanding requests at 40 cycles of latency completes at most 8/40 = 0.2 per cycle. Raise the limit to 20 to sustain the offered rate.

    The design space

    Option

    What it costs

    When it wins

    B ≥ worst-case RTT × rate

    Buffer area proportional to bandwidth × distance

    The link must run at full rate under all conditions

    Smaller B, accept less throughput

    Peak rate drops to B / RTT

    The traffic never needs the full rate (for example, a control channel)

    Shorten the round trip

    Design effort: return credits as soon as a slot frees, give credit returns priority, cut return-path logic

    Area is tight; every cycle removed saves one flit of buffer per flit-per-cycle of bandwidth

    Share one buffer pool among traffic classes

    Needs rules so one class cannot take every credit (lesson 4.6)

    Many classes, each bursty, rarely all busy at once

    Batch credit returns

    Fewer return messages, but each credit comes back later, which lengthens RTT (lesson 4.7)

    Return-path bandwidth is scarce

    How real protocols chose

    Protocol

    Its constraint

    Its choice

    PCI Express

    Chip-to-chip links with long round trips, several traffic types that must not block each other

    Six credit types: posted, non-posted and completion, each with header and data credits. Data credits count 16-byte units. Credits are advertised at link initialisation and returned with UpdateFC packets; a packet waits until enough credits are available. Endpoints advertise infinite completion credits, so completions are never held back for lack of credit.

    AXI

    On-chip channels where the far side usually answers within a cycle

    No credits on the wire; VALID/READY on each channel. When a long route needs pipeline stages, each stage needs its own buffering (register slices and skid buffers, Part 3), which is the same round-trip sizing problem in a different form.

    On-chip networks

    Many hops, many traffic classes, buffers at every router

    Credit-based flow control per link and per virtual channel is the common choice, with each router's input buffer sized to its link's credit round trip.

    What breaks

    A late timing fix adds two pipeline stages in each direction to a long route between two subsystems (illustrative numbers). Before the change, L = 3 and the receiver buffer is B = 8: RTT = 2 × 3 + 2 = 8, exactly covered, so the link runs at 100%. After the change, L = 5, RTT = 12, and the same buffer gives 8 / 12 = 67%. Functional regressions all pass, because a slower link is still a correct link. The drop appears weeks later as a performance regression with no failing test.

    The evidence is in the waveform: bursts of exactly B sends separated by gaps, while the receiver's buffer drains and sits empty. That signature, sender out of credits while the receiver is idle, always means the loop is longer than the buffer. The fix is a 12-entry buffer (B = RTT = 12). Shortening the loop helps only in proportion: returning credits a cycle earlier at each end brings RTT to 10, so 8 / 10 = 80%, better but not full rate. Without a bigger buffer, full rate needs RTT back to 8, which means taking all four added cycles back out of the loop, in either direction. The lasting fix is a sizing assertion: a check that compares the buffer parameter with the round trip the integration actually built, so the next timing fix cannot silently break it.

    Why this matters

    Principal lens. Buffer depth is a product of two numbers that different teams own: bandwidth (the architect) and round-trip latency (the physical designers, the integrators, whoever adds a pipeline stage next quarter). That is why credit loops regress quietly. In a review, ask for the round trip as a written budget, cycle by cycle, including return arbitration and any clock crossing, and ask what happens to throughput when the budget grows by 20%. At high bandwidth the buffer becomes SRAM and a floorplan concern, so the cheapest fix is usually on the latency side: return credits as early as correctness allows, and never batch them on a link that is already short of buffering.

    Interview

    Asked: "The link has 40 cycles of credit round trip. How deep must the receiver buffer be?"

    Weak answer: Deep enough for the largest burst, maybe 16 entries; the receiver drains quickly, so a big buffer is wasteful.

    Strong answer: At one flit per cycle, at least 40 flits, because each credit is reused at most once per round trip, so throughput is min(1, B/40). I would confirm what the 40 cycles include (return arbitration, crossings, the drain cycle) and size from the worst case. If the receiver drains at only half rate, about 20 credits are enough to sustain that rate.

    Follow-ups: (1) Two traffic classes share the link. Do you split the buffer or share it? (2) How would you detect a credit leak in silicon?

    Check yourself A link has L = 6 and B = 10 in the reference model. What throughput do you expect, and what B gives full rate?

    RTT = 2 × 6 + 2 = 14 cycles, so throughput = 10 / 14 ≈ 71%. Full rate needs B ≥ 14.

    Check yourself A designer doubles the link width (twice the bytes per flit) without changing the clock or the route. What happens to the required buffer in bytes and in flits?

    In flits nothing changes: the round trip in cycles is the same, so B ≥ RTT still holds. In bytes the buffer doubles, because each flit is twice as large and the bandwidth doubled. Bandwidth × RTT doubles.

    Check yourself The waveform shows the sender with zero credits for long stretches while the receiver buffer is empty. A colleague proposes making the receiver drain faster. Will it help?

    No. An empty buffer means the receiver is already draining everything it gets. The credits are in flight on the return path. Only a larger buffer (more credits) or a shorter round trip raises throughput.

    Key takeaways

    • With credit flow control, throughput = min(1, B / RTT): each credit can be used at most once per round trip.

    • Size the buffer from the worst-case credit round trip, counted cycle by cycle, times the rate you must sustain: bandwidth × RTT in bytes.

    • Sender out of credits while the receiver sits idle means the loop is longer than the buffer; check the invariant and the sizing, not the receiver.

    Keep going

    Next: Lesson 4.6, Credit Classes and Pools. What changes when several kinds of traffic share one link, and why PCIe counts header and data credits separately.

    Related topics

    • Part 4 overview: Flow Control

    • Architecture course: Credit Flow Control and Buffers

    • Lesson 3.3: The Valid/Ready Handshake

    Previous

    55 / 234

    Next: Credit Classes and Pools

    Up next

    Up nextCredit Classes and Pools

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Why Flow Control
    • On/Off Flow Control
    • Credit-Based Flow Control
    clkcredits32101010sendrx_vldpopcrd_in
    Simulation output of the listing above: L = 2, B = 3, RTT = 6. The sender sends three flits, waits three cycles for credits to come home, and repeats. One slot is one clock cycle.