A new signal needs to cross and three mechanisms compete. Pick the FIFO for a stream and it idles half empty; pick two flops for a bus and bits arrive on different cycles; pick a handshake for a fast stream and throughput collapses. The choice follows from what the signal is, not from taste, and its size follows from arithmetic. This lesson states the rules and prices both answers. Its one job is the choice and its sizing; the review that checks it is next.
One bit that is already a level, held long enough, crosses on two flops: resets, enables, lock flags, interrupt lines. One bit that moves as a pulse crosses on a toggle or a handshake, because a pulse shorter than the destination period may never be sampled. Correlated multi-bit data, a bus whose bits must arrive together, crosses on a handshake when traffic is occasional and on an asynchronous FIFO when it streams. Counters cross gray-coded so only one bit moves per step. Anything else, a bus with independent two-flop synchronisers per bit, is the classic multi-bit error: the destination samples bits from different source cycles and reads a value that never existed.
For the crossing circuits themselves see the CDC course: handshake variants, pulse synchronisers and the synchroniser selection guide. The integrator's arithmetic is twofold. FIFO depth must cover the burst that arrives while the read side drains, plus the synchroniser latency allowance, plus margin. Synchroniser reliability follows the MTBF exponential: each extra stage adds about a clock period to the resolving time, and the formula needs technology constants that the integrator takes as inputs, never invents.
Worked case: a store burst of 4 beats on the W channel of X1, written on clk_cpu at 192 MHz and read on clk_sys at 96 MHz through the 4-deep FIFO of nano_axi_cdc_m. The formula gives 4 x (1 - 96/192) + 2 + 1 = 5 entries to absorb the burst with no stall, one more than the FIFO has. That is not a defect. The FIFO never overflows because the write side stops on full: the writer backpressures, which costs throughput, not data. A cache line fill goes the other way: it returns on the R channel (X2), written at 96 MHz and read at 192 MHz, so the fast reader drains it as it lands and capacity is no concern. The model below measures the peak before draining, shows that peak 4 means exactly full with zero margin, and shows that without flow control the same FIFO would lose data on a longer burst.
# FIFO depth: burst x (1 - f_read/f_write) + allowance (illustrative: 2 for
# synchroniser latency, 1 margin). Case: 4-beat store burst on W, X1.
BURST, F_WRITE, F_READ = 4, 192e6, 96e6
need = BURST * (1 - F_READ / F_WRITE) + 2 + 1
assert need == 5.0, need # more than the 4-deep NanoSoC-specification FIFO
def run(depth, beats, read_start, flow_control):
# One step per fast (write) cycle; the slow reader pops every 2nd cycle
# once read_start cycles of synchroniser latency have passed.
occ = peak = written = lost = stalls = 0
cycle = 0
while written + lost < beats:
if occ == depth:
if flow_control:
stalls += 1 # writer waits: no data lost
else:
lost += 1 # beat written into a full FIFO
written += 0
else:
occ += 1
written += 1
peak = max(peak, occ) # peak measured BEFORE draining
if cycle >= read_start and (cycle - read_start) % 2 == 1 and occ > 0:
occ -= 1
cycle += 1
return peak, stalls, lost
# 4-beat burst, reads start after 4 fast cycles of sync latency: peak 4.
assert run(4, 4, 4, False) == (4, 0, 0) # exactly full, zero margin
# A longer burst of 8 beats overflows an 4-deep FIFO without flow control...
assert run(4, 8, 4, False)[2] > 0
# ...but with full/empty flow control the writer stalls and nothing is lost.
peak, stalls, lost = run(4, 8, 4, True)
assert lost == 0 and stalls > 0, (peak, stalls, lost)
# A deeper FIFO removes the stalls for the same burst.
assert run(8, 8, 4, True)[1] == 0
print("PASS: need 5 vs 4-deep; peak 4 is exactly full; flow control stalls, never loses")# Synchroniser MTBF MTBF = e^(t_res/tau) / (Tw f_c f_d).
# Tau and Tw are illustrative technology inputs, not NanoSoC measurements.
import math
TAU_PS, TW_PS = 20.0, 10.0 # illustrative inputs, labelled as such
F_CLK, F_DATA = 192e6, 96e6
T_CLK_PS = 1e12 / F_CLK
def mtbf_years(stages):
t_res_ps = T_CLK_PS - 100.0 + (stages - 2) * T_CLK_PS # setup illustrative
mtbf_s = math.exp(t_res_ps / TAU_PS) / ((TW_PS / 1e12) * F_CLK * F_DATA)
return mtbf_s / (3600 * 24 * 365)
assert mtbf_years(2) > 1e6, mtbf_years(2) # two stages: comfortable
assert mtbf_years(3) > mtbf_years(2) # each stage multiplies the MTBF
print("PASS: 2-flop MTBF far above need with illustrative constants; 3 flops higher")Every new crossing states its signal class first and its mechanism second; the class decides.
FIFO depths show the burst arithmetic with the synchroniser allowance and margin as separate lines.
MTBF numbers name their tau and Tw inputs and label them illustrative; no MTBF is quoted without its constants.
Stress each FIFO with continuous bursts at the worst ratio while the read side stalls at random: overflow or underflow convicts the depth. Sweep pulse crossings with ratio and phase sweeps: a pulse caught at one alignment and missed at another needs a toggle, not a faster clock. A depth too small for the burst under the ratio is caught by ratio-sweep simulations.
Together at the source still arrives skewed: each bit resolves through its own synchroniser with independent latency, so the destination can sample some bits from the new value and some from the old. Correlated bits need a handshake or a FIFO.