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 Path
  • Cache Coherency Glossary
  • The Shared Course Model
  • Cache Coherency Revision Sheet
  • Why Coherence Exists8
  • States and Protocols12
  • Transactions12
  • Races and Controller Design8
  • Progress and the Fabric16
  • Ordering and Synchronization12
  • Performance and Scalability12
  • Real Protocols12
  • SoC Integration12
  • Power, Clocking and Reliability12
    • Power State Transitions and Cache State Safety
    • Clocking and CDC on Coherence Channels
      • Coherence Channels Across Clocks
      • Credits Across Clock Domains
      • Quiesce and Link Reset
      • CDC Bugs in Coherence
    • ECC, Poison and Recovery
  • Verification12
  • Debug and Bring-up12
  • Interview and Design Review17
  • Capstone: A Coherent Model Lab5
Loading lesson

‹Clocking and CDC on Coherence Channels
  1. Topics
  2. Cache Coherency
  3. Power, Clocking and Reliability
  4. Clocking and CDC on Coherence Channels
  5. Coherence Channels Across Clocks

Part 10 · Power, Clocking and Reliability · All levels

Coherence Channels Across Clocks

One asynchronous FIFO per message class and direction, why classes must stay separate across the crossing, and what synchronization adds to a miss.

On this page

    What happens to GetM when C0 and H have different clocks?

    A message is many bits: type, address, transaction identity and sometimes a 64-byte payload. Sampling those bits directly in another clock domain can mix old and new values. The standard structure is an asynchronous FIFO: the sender writes the whole message into a storage slot in its own clock domain, and only the FIFO pointers cross. Each pointer is Gray-coded, so one bit changes per increment, and passes through a two-flop synchronizer in the other domain. The receiver reads a slot only after the synchronized write pointer shows it is filled; by then the slot's bits have been stable for at least two receiver cycles. The async FIFO lessons and Gray pointers in sign-off teach the design; here it is a box with one rule.

    The rule for coherence: one asynchronous FIFO per message class and direction. Cluster to H carries requests (GetS, GetM, PutS, PutE, PutM) and responses (Inv-Ack, Data from a forwarding cache). H to cluster carries forwards (Inv, Fwd-GetS, Fwd-GetM) and responses (Data, Put-Ack). That is four FIFOs. Within a class the FIFO keeps order, but the protocol must not depend on it (the course assumes no FIFO order), and classes may overtake each other freely.

    The cluster domain with C0 and C1 sends requests and responses to H through two separate FIFOs, and H sends forwards and responses back through two more.
    Four asynchronous FIFOs, one per class and direction. Each has its own Gray-coded pointers and two-flop synchronizers.

    The same care applies to the coherency-entry handshake that sits beside the channels. The CHI SYSCOREQ/SYSCOACK handshake signals must be synchronous to the interface clock, and a bridge that crosses a clock domain must synchronize them (Arm IHI 0050 Issue E.a, section 15.1).

    Why a shared FIFO brings the deadlock back

    Part 5's rule is that responses always sink: consuming a response never waits on a resource that a request can hold. A Protocol Deadlock broke its P/Q cycle by giving X's Data a reserved slot Qresp. Now put that design behind a crossing. If the cluster-to-H direction uses one shared FIFO, the FIFO itself is the old shared Q: it is strictly in order, so H can read only its head.

    Keep the roles of the linked lesson (MSI, X warm-up: C0 stored 1, memory = 0). C1's GetM(X) holds H's only pending entry P, C0 answered Fwd-GetM and gave up X, and C0's next miss GetS(Y) entered the crossing before C0's Data(1). The cache states at that checkpoint are exactly the linked lesson's step 3:

    Step

    C0 state / value

    C1 state / value

    H directory

    Memory

    Newest data

    3 GetS(Y) ahead of Data(1) in the crossing

    X: I / -, Y: IS^D / -

    X: IM^D / -, Y: I / -

    X: exclusive(C0), pend GetM C1, Y: U

    X = 0, Y = 0

    Data(1) in flight to H (in the crossing FIFO), memory: Y = 0

    Crossing design

    FIFO contents, head first

    What H can do

    Result

    One shared FIFO

    GetS(Y), Data(X=1)

    Head GetS(Y) needs a free P, P waits for Data(X), which is behind it

    Deadlock. A deeper FIFO does not help: the head still blocks.

    Request FIFO and response FIFO

    Req: GetS(Y). Resp: Data(X=1)

    Reads Data(X) from the response FIFO, completes C1's grant, frees P, then reads GetS(Y)

    Both complete.

    The same holds in the other direction: a forward stuck behind a blocked response, or a response behind a forward the cache cannot accept yet, rebuilds a cycle through the cache controller. Separate FIFOs are necessary but, as in Message Classes and Virtual Networks, not sufficient: the response FIFO also needs space when a response arrives. That is a credit question, the next lesson.

    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
    def run(order, shared, depth):
        """Cluster-to-H crossing. P is held by C1's GetM(X) until Data X arrives."""
        fifos = {"req": [], "resp": []}
        waiting = list(order)               # messages the cluster still has to write
        p_free, done = False, set()
        def fifo_of(m):
            return "req" if shared or m == "GetS Y" else "resp"
        while True:
            moved = False
            while waiting and len(fifos[fifo_of(waiting[0])]) < depth:
                m = waiting.pop(0)
                fifos[fifo_of(m)].append(m)
                moved = True
            for name in ("resp", "req"):    # H looks only at each FIFO's head
                if not fifos[name]:
                    continue
                head = fifos[name][0]
                if head == "Data X":        # response: uses the existing P
                    fifos[name].pop(0)
                    p_free = True
                    done.add("X")
                    moved = True
                elif head == "GetS Y" and p_free:
                    fifos[name].pop(0)
                    done.add("Y")
                    moved = True
            if not moved:
                return done
    orders = [("GetS Y", "Data X"), ("Data X", "GetS Y")]
    for depth in range(1, 9):
        for order in orders:
            assert run(order, shared=False, depth=depth) == {"X", "Y"}
        # failing case: one shared FIFO, request ahead of the response
        assert run(orders[0], shared=True, depth=depth) == set()
        assert run(orders[1], shared=True, depth=depth) == {"X", "Y"}
    print("PASS: per-class FIFOs complete X and Y, a shared FIFO of any depth can deadlock")

    The check runs depths 1 to 8. Separate FIFOs complete both transactions in both arrival orders. The shared FIFO completes only when the response happens to be ahead, and never when GetS(Y) is at the head, at any depth.

    Latency: what the synchronizers add to a miss

    A message written into an asynchronous FIFO becomes readable only after the write pointer has passed the receiver's two synchronizer flops, plus up to one more receiver cycle because the clocks have no fixed phase. The full and empty flags are therefore conservative: the receiver sees a new entry late, the sender sees a freed slot late, never early. Take an illustrative 3 cycles per crossing, in the same illustrative cycle unit as Anatomy of a Miss Latency.

    Transaction (linked timing)

    Legs that cross the boundary

    Without crossing

    With crossing, illustrative

    Upgrade from Shared, C0 upgrades, C1 invalidated

    GetM, Inv, Inv-Ack, Data: 4

    20 cycles

    20 + 4 x 3 = 32 cycles

    Cold read miss, data from memory

    GetS, Data: 2

    depends on memory

    memory path + 2 x 3 cycles

    Every serial leg pays the crossing once, so a protocol with more serial legs pays more. A three-hop transfer between C0 and C1 inside the cluster domain pays for the request and forward legs only, since the Data leg stays in the cluster; the H-collected upgrade pays four times. Where the clocks have a known fixed ratio, designers can use cheaper crossings, but that is a clocking decision, not a protocol one.

    Common pitfall

    Merging a low-traffic class into a busy one at the crossing to save a FIFO. The saving is a few slots, the cost is the Part 5 deadlock, and CDC sign-off will not report it because every bit still crosses correctly.

    Interview

    Draw the four FIFOs, say which classes cross in each direction, then show the shared-FIFO head-of-line cycle on the P/Q example and add the latency: number of serial legs times the crossing penalty.

    Check yourself Could you keep one physical FIFO and fix the deadlock by letting H read any entry, not just the head?

    Then it is no longer a FIFO but a shared buffer, and the capacity problem remains: requests can fill every slot so the response cannot enter at all. Responses still need storage that requests cannot occupy, which is what a separate per-class FIFO, or a reserved slot with its own credits, gives.

    Related topics

    • Shared course model

    • The P/Q cycle this lesson moves into the crossing

    • CDC course: asynchronous FIFOs

    • CDC course: two-flop synchronizers

    • Latency breakdown being extended

    Previous

    114 / 167

    Next: Credits Across Clock Domains

    Up next

    Up nextCredits Across Clock Domains

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Quiesce and Link Reset
    • CDC Bugs in Coherence