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
    • Common-Clock Interfaces
    • Asynchronous Serial and Oversampling
    • Source-Synchronous Clocks and Strobes
    • Embedded Clocks and Clock Recovery
    • Skew and Deskew
    • Clock Relationships
    • Elastic Buffers and Clock Compensation
    • Self-Timed Handshakes
    • Crossing at the Interface
    • Protocols Compared: Timing and Clocking
    • Drills: Timing and Clocking
  • Part 2: Encoding and Framing12
  • Part 3: Handshakes13
  • Part 4: Flow Control17
  • 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 1: Timing and Clocking
  1. Topics
  2. First Principles of Hardware Protocols
  3. Part 1: Timing and Clocking
  4. Elastic Buffers and Clock Compensation

Part 1 · Timing and Clocking · Senior (S)

Elastic Buffers and Clock Compensation

How a serial receiver absorbs the ppm difference between two clocks: an elastic buffer kept half full by deleting or repeating filler symbols that carry no data.

On this page

    Note

    Level: Senior (S) · Answers: When is the data valid?

    Before this: Clock Relationships

    A serial receiver recovers the transmitter's clock, so it can capture every symbol. But the rest of the receiving chip runs on its own clock, which differs by a few hundred ppm. Symbols arrive slightly faster (or slower) than the chip consumes them, about one extra symbol every couple of thousand. A FIFO between the two clocks fills up (or runs dry) in a few milliseconds, however deep it is.

    How does a receiver hand a continuous stream from one clock to a slightly different one, forever, without losing or inventing data?

    The obvious answer, and why it breaks

    The obvious answer is an asynchronous FIFO between the recovered clock and the local clock. It handles the crossing safely, but it cannot change the rate: if the writer is 600 ppm faster than the reader, the FIFO gains a symbol every 1,667 cycles, and any finite depth eventually overflows. Making it deeper only delays the failure. Something must actually remove symbols when the writer is fast and add symbols when it is slow, and it must never be data.

    The idea: filler symbols the receiver may delete or repeat

    Two pieces work together:

    • The protocol makes the transmitter send filler symbols at regular intervals: symbols that carry no information, so removing or duplicating one changes nothing. Serial links send them in small groups, often called skip (SKP) symbols.

    • The receiver has an elastic buffer between the recovered clock and its local clock, and it watches how full the buffer is. When the buffer is filling beyond its midpoint, it deletes a filler symbol from a group as it passes. When it is draining below the midpoint, it inserts one by reading a filler symbol twice. Data symbols are never touched.

    The buffer stays near half full, and the stream's real content passes through unchanged. The transmitter does not know or care whether its filler was shortened or stretched.

    The receiver's elastic buffer absorbs the clock difference by adjusting only filler symbols.

    How often must filler appear?

    Each filler group gives the receiver one chance to remove or add a symbol. The clocks gain one symbol on each other every 1/Δ symbols, where Δ is the fractional frequency difference (lesson 1.6). So groups must come at least that often, with margin:

    Diagram
    600 ppm difference -> one symbol of drift every 1 / 0.0006 = 1,667 symbols
    Filler group every K symbols, one adjustment per group -> need K < 1,667 (with margin)
    
    Buffer depth: enough above and below the midpoint to ride out the drift between groups,
    plus the receiver's own pipelining. A filler group every 1,180 symbols (illustrative)
    means at most 1,180 x 0.0006 = 0.7 symbols of drift between chances to correct it.

    Build it: a buffer between two clocks

    The model writes symbols at the far clock's rate and reads one per local cycle. The transmitter inserts a group of three filler symbols (S) after every K data symbols, and the receiver deletes or repeats one filler per group to stay near half of a 16-entry buffer. It checks that the data comes out complete and in order, then runs once more with no filler at all.

    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
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    """An elastic buffer between two clocks that differ by a few hundred ppm.
    
    The transmitter's clock writes symbols into the buffer; the receiver's clock reads
    one symbol per cycle. Every K data symbols the transmitter sends a group of three
    SKP symbols that carry no data. When the buffer is filling up, the receiver deletes
    one SKP from a group; when it is draining, it inserts one by reading a SKP twice.
    Data symbols are never added or dropped.
    """
    from collections import deque
    
    DEPTH, MID = 16, 8
    
    
    def run(write_ppm, cycles, K):
        rate = 1 + write_ppm * 1e-6        # writer symbols per reader cycle
        buf = deque(["S"] * MID)           # start half full
        acc, next_data, since_skp = 0.0, 0, 0
        pending = deque()                  # symbols the writer will send, one per writer cycle
        sent, out = [], []
        deleted = inserted = 0
        fill_trace = []
        group_adjusted = False
        for cyc in range(cycles):
            acc += rate
            while acc >= 1:                # writer side: 0, 1 or 2 symbols this reader cycle
                acc -= 1
                if not pending:
                    if since_skp >= K:
                        pending.extend(["S", "S", "S"]); since_skp = 0
                    else:
                        pending.append(next_data); sent.append(next_data); next_data += 1; since_skp += 1
                buf.append(pending.popleft())
            if len(buf) > DEPTH:
                return dict(failed=f"overflow at cycle {cyc}", trace=fill_trace)
            if not buf:
                return dict(failed=f"underflow at cycle {cyc}", trace=fill_trace)
            head = buf[0]
            if head != "S":
                group_adjusted = False
            if head == "S" and not group_adjusted and len(buf) > MID + 1 and len(buf) > 1:
                buf.popleft(); deleted += 1; group_adjusted = True     # delete one SKP
                head = buf[0]
            if head == "S" and not group_adjusted and len(buf) < MID - 1:
                out.append("S"); inserted += 1; group_adjusted = True  # insert: read it again
            else:
                out.append(buf.popleft())
            if cyc % 2000 == 0:
                fill_trace.append(len(buf))
        data_out = [x for x in out if x != "S"]
        assert data_out == sent[: len(data_out)]                       # data intact, in order
        return dict(failed=None, deleted=deleted, inserted=inserted, trace=fill_trace)
    
    
    for ppm in (+600, -600):
        r = run(ppm, 200_000, K=1180)
        assert r["failed"] is None
        t = r["trace"]
        print(f"writer {ppm:+d} ppm, SKP every 1180 symbols: fill stayed {min(t)}..{max(t)}, "
              f"{r['deleted']} SKP deleted, {r['inserted']} inserted")
    
    r = run(+600, 30_001, K=10**9)
    print(f"writer +600 ppm, no SKP symbols: {r['failed']}")
    print("fill every 2000 cycles without SKP:", r["trace"])
    print("fill every 2000 cycles with SKP:   ", run(+600, 30_001, K=1180)["trace"])
    print("PASS: SKP deletion and insertion keep the buffer near half full; without them it overflows")

    It prints:

    Diagram
    writer +600 ppm, SKP every 1180 symbols: fill stayed 8..9, 119 SKP deleted, 0 inserted
    writer -600 ppm, SKP every 1180 symbols: fill stayed 5..7, 0 SKP deleted, 118 inserted
    writer +600 ppm, no SKP symbols: overflow at cycle 13333
    fill every 2000 cycles without SKP: [8, 9, 10, 11, 12, 14, 15]
    fill every 2000 cycles with SKP:    [8, 9, 8, 8, 8, 9, 8, 9, 8, 8, 9, 9, 8, 9, 8, 9]
    PASS: SKP deletion and insertion keep the buffer near half full; without them it overflows
    The model's fill levels every 2,000 cycles. Without filler symbols the buffer gains about one symbol every 1,667 cycles and overflows its 16 entries; with them it stays at 8 or 9.

    With a fast writer the receiver deleted 119 filler symbols in 200,000 cycles, one per slip (200,000 / 1,667 ≈ 120); with a slow writer it inserted 118. Without filler, the buffer overflowed at cycle 13,333: it started 8 symbols below full, and 8 / 0.0006 = 13,333. Depth bought time, not a solution.

    The design space

    Option

    What it costs

    When it wins

    Elastic buffer with filler adjustment (this lesson)

    Filler bandwidth; a buffer; the protocol must define filler symbols

    Continuous serial streams between plesiochronous clocks

    Shared reference clock on both ends

    A clock distributed to both ends

    Boards and systems that can route one reference; the ppm difference shrinks (often the buffer still exists for safety)

    Run the receiver logic on the recovered clock

    The whole receiver in another clock domain

    Simple devices where the data leaves on the same clock it arrived with

    Async FIFO with idle gaps the receiver can drop

    Needs gaps in traffic

    Packet links with idle time between packets

    How real protocols chose

    Protocol

    Its choice

    PCI Express

    Receivers compensate for clock differences with an elastic buffer that adds or removes SKP symbols in SKP ordered sets, which transmitters send at regular intervals. The two ends may differ by up to 600 ppm in PCIe 1.0–4.0.

    UART

    No continuous stream, so no compensation: each frame resynchronises on its start bit (lesson 1.2).

    What breaks

    A transmitter optimisation suppresses filler groups while a long packet is being sent, to squeeze out a little more bandwidth (illustrative). Long packets now run for more than 1/Δ symbols with no filler, and the receiver's elastic buffer drifts beyond its limit during the packet, dropping or duplicating a data symbol. The link layer's CRC then rejects the packet and it is replayed, so the bug shows up only as a rising replay count under heavy traffic.

    • Symptom: replays increase with packet length and traffic, not with error-prone channels.

    • Evidence: elastic buffer overflow or underflow events during long packets.

    • Fix: send filler at the interval the specification requires, even in the middle of a burst; the filler interval is a promise to the receiver, not an option.

    Why this matters

    Senior lens. Clock compensation is a contract between the two ends: the transmitter promises filler at a maximum interval, the receiver promises a buffer deep enough for the worst ppm difference between fillers. Check both sides against the worst case, including spread-spectrum clocking if the system uses it, which can raise the effective difference far above the oscillator tolerance. Expose elastic buffer overflow and underflow as error counters, because a compensation failure otherwise looks like random bit errors.

    Interview

    Asked: "Why does PCI Express send SKP ordered sets?"

    Weak answer: To keep the link active.

    Strong answer: For clock compensation. The two ends run from clocks that may differ by hundreds of ppm, so the receiver's recovered clock and its local clock drift by about one symbol every couple of thousand. Its elastic buffer between the two absorbs that by deleting a SKP symbol when it's filling or repeating one when it's draining, because SKP carries no data. Without them, any buffer depth would eventually overflow or underflow.

    Follow-ups: (1) How often must the filler appear for a 600 ppm difference? (2) Why can't a deeper async FIFO solve the problem?

    Check yourself Two clocks differ by 300 ppm. What is the longest safe interval between filler groups if each group allows one adjustment?

    One symbol of drift every 1 / 0.0003 ≈ 3,333 symbols, so groups must come more often than that, with margin for the buffer's pipelining.

    Check yourself Why does the receiver delete or repeat only filler symbols, never data?

    Filler carries no information, so removing or duplicating it leaves the stream's content unchanged. Touching a data symbol would corrupt the data.

    Check yourself In the model, the slow-writer case inserted 118 symbols. What would happen if inserting were not allowed?

    The buffer would drain by about one symbol every 1,667 cycles and underflow, leaving the reader with nothing valid to read.

    Key takeaways

    • A deeper FIFO cannot fix a rate difference; only removing or adding symbols can.

    • The transmitter sends filler symbols regularly; the receiver's elastic buffer deletes or repeats them to stay half full.

    • Filler must come more often than once per 1/Δ symbols, where Δ is the clock difference; the interval is a promise to the receiver.

    Keep going

    Next: Lesson 1.8, Self-Timed Handshakes. The clockless alternative: let each transfer's own handshake say when the data is valid.

    Related topics

    • Part 1 overview: Timing and Clocking

    • CDC & RDC course: Async FIFO CDC

    Previous

    21 / 234

    Next: Self-Timed Handshakes

    Up next

    Up nextSelf-Timed Handshakes

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Common-Clock Interfaces
    • Asynchronous Serial and Oversampling
    • Source-Synchronous Clocks and Strobes