An engineer joins a team building a serial link controller and is asked a simple question in the first design review: "How many clock domains does the receive path cross, and where?" The block diagram on the wall shows a PHY, a link layer and an application interface, with no clocks drawn at all.
Where do clock crossings sit inside a protocol stack, and what makes each one safe?
The obvious answer is "one crossing, at the edge of the chip": data comes in on the link's clock and goes out on the chip's clock. In practice a receive path usually crosses at least twice, for different reasons, and each crossing needs a different mechanism. Treating them as one, or placing a crossing where the protocol gives it nothing to work with, produces the hardest bugs a link can have: rare, rate-dependent and invisible in single-clock simulation.
Follow a symbol from the wire to the application in a typical serial receiver:
Recovered clock → local link clock. The CDR delivers symbols on the transmitter's timing (lesson 1.4). The two clocks are plesiochronous, so the crossing must also absorb a rate difference. The protocol makes that safe by sending filler symbols, and the receiver uses an elastic buffer (lesson 1.7).
Link clock → application clock. The link layer runs on a clock tied to the line rate; the application (a bus such as AXI, or the rest of the chip) runs on its own, unrelated clock. This crossing carries whole packets or transactions, so an async FIFO with flow control (credits or ready) is the usual answer. The protocol makes it safe by having flow control that tolerates the FIFO's extra latency (Part 4).
Control and status signals. Single-bit events (link up, error, interrupt) cross with synchronisers or pulse synchronisers; multi-bit status crosses with a handshake. The protocol's job is to make these quasi-static or event-like, never fast-changing buses.
Signal or stream | From → to | Relationship | Crossing | What the protocol provides |
|---|---|---|---|---|
Received symbols | Recovered → link clock | Plesiochronous | Elastic buffer | Filler symbols at a guaranteed interval |
Received packets | Link → application clock | Asynchronous | Async FIFO | Flow control that tolerates the FIFO latency |
"Link is up" | Link → application clock | Asynchronous | Two-flop synchroniser | A level that changes rarely |
"Packet dropped" event | Link → application clock | Asynchronous | Pulse (toggle) synchroniser | Events spaced far enough apart |
Error counter value | Link → application clock | Asynchronous | Handshake, or Gray-coded counter | A value read occasionally, not a live bus |
Notice the last column: every crossing depends on something the protocol guarantees. Designing the crossing and designing the protocol are the same activity seen from two sides.
Placement | What it costs | When it wins |
|---|---|---|
Cross early (near the PHY), run the link layer on the application clock | Link layer must keep up with the line rate on a clock it does not control | Application clock comfortably faster than the line's symbol rate |
Cross late (at the application interface), run the link layer on the link clock | An async FIFO at the transaction boundary; flow control must cover its latency | Most controllers: the link layer stays tied to the line |
Cross in the middle (between transaction and link layers) | Two well-defined interfaces to keep consistent | Controllers with separate PHY and protocol vendors |
Protocol | Crossings |
|---|---|
PCI Express | An elastic buffer at the receiver absorbs the up to 600 ppm difference (PCIe 1.0–4.0) by adding or removing SKP symbols; credit flow control on the link tolerates the latency of whatever buffering follows. |
DDR memory | Read data is captured with its strobe (DQS) in the memory's timing and then handed to the controller's clock, so every read crosses from the strobe's timing into the controller's domain. |
UART | The line is asynchronous to the receiver, so it passes through a synchroniser before the oversampling logic (lesson 1.2). |
A team connects a link layer to an application bus running on another clock and, to save area, passes the received packet's header fields across with only a two-flop synchroniser on each bit (illustrative). Most of the time the header is stable long before it is used. Occasionally the application samples while bits are changing, and different bits land in different cycles: a header with half the old packet's fields and half the new one's. The fix is a proper crossing for multi-bit data, an async FIFO or a handshake, and the CDC course has an entire section on why bit-by-bit synchronisation of buses fails.
The first crossing must absorb a rate difference in a continuous stream, which only filler adjustment can do. The second carries packets between unrelated clocks with flow control, so ordinary back-pressure can keep the FIFO from overflowing.
No. A slowly changing level can use a two-flop synchroniser. A short pulse could be missed by the slower side, so it needs a pulse (toggle) synchroniser, and the protocol must space such events far enough apart.
Flow control that tolerates the FIFO's latency, so the sender stops before the FIFO fills even though its "full" indication arrives several cycles late (Part 4).
A link's receive path usually crosses clock domains more than once: recovered to link clock, and link to application clock.
Each crossing uses what the protocol guarantees there: filler symbols for an elastic buffer, flow control for an async FIFO, slow-changing signals for synchronisers.
Never synchronise a multi-bit value bit by bit; use a FIFO or a handshake.