The CPU runs at 192 MHz and the fabric at 96 MHz, and signoff treats the two as asynchronous despite the shared PLL: different divider roots mean uncontrolled skew. Every fetch the CPU makes crosses that boundary twice. If the crossing is a bare synchroniser on a multi-bit bus, the destination occasionally samples a mixed value and executes garbage. If it is a FIFO, a full FIFO does not overflow: with valid and ready, it drops ready and the sender waits. The question this lesson answers is therefore how much the FIFO depth costs in throughput, and whether the CPU ever notices.
The nano_axi_cdc_m crosses with one 4-deep dual-clock FIFO per channel, gray-coded pointers and two-flop pointer synchronisers: multi-bit data never crosses without the FIFO, and no single-bit control crosses without two flops. That is the standing rule for all ten NanoSoC crossings. The clock ratio is fixed at 2 to 1, so there is no ratio to sweep; the numbers below are exact.
Sizing starts from the rates. The source side can offer 4 x 192 = 768.0 MB/s, the fabric side takes 4 x 96 = 384.0 MB/s, and the slow side limits. At the full source rate and a 7 clk_sys loop (request crossing 3, xbar address 1, memory 2, xbar data 1) there are 0.875 of a 16-beat burst in flight, less than one burst. A FIFO that could swallow a whole burst without back-pressure would need about 16 x (1 - 384/768) = 8 entries plus a synchroniser allowance, and NanoSoC deliberately does not provide that. With 4 entries the sender may have 4 words outstanding per round trip, which bounds a streaming master at about 4 x 4 B / 72.9 ns = 219 MB/s (an illustrative window estimate). The CPU is a blocking master that never asks for more than 128 MB/s (line fills) or 42.67 MB/s (single words), so 4 entries never slow it. A streaming master added later would be limited by this FIFO before it was limited by the fabric.
Width conversion is the dog that does not bark here: every fabric link is 32 bits, the DMA stream is 32 bits and APB is 32 bits, so WSTRB passes through untouched and no upsizer or downsizer can corrupt neighbouring bytes. Endianness is fixed the same way: the whole chip is little endian, and any future big-endian block would get its lane swizzle in its own wrapper, verified with byte and halfword accesses because full-word tests pass straight through a swizzle bug.
# NanoSoC teaching values: what the 4-deep FIFOs in the CPU bridge can sustain.
T_SYS = 1e9 / 96e6 # ns
SRC_MBS = 4 * 192.0 # CPU side, 32 bits at 192 MHz = 768.0 MB/s
SINK_MBS = 4 * 96.0 # fabric side, 384.0 MB/s: the slow side limits
LOOP = 7 * T_SYS # ns: request crossing, fabric and memory (7 clk_sys)
BURST_BYTES = 64 # 16 beats of 4 bytes
in_flight_bursts = SRC_MBS * 1e6 * LOOP * 1e-9 / BURST_BYTES
assert abs(in_flight_bursts - 0.875) < 0.001 # less than one burst in flight
def window_limit_mbs(depth_words):
# a full FIFO drops ready: depth words per round trip bounds the rate
return min(SINK_MBS, depth_words * 4 / (LOOP * 1e-9) / 1e6)
CPU_LINE_FILL = 128.0 # MB/s, the fastest the blocking CPU ever asks for
CPU_WORD = 42.67 # MB/s, single-word reads
assert window_limit_mbs(4) > CPU_LINE_FILL > CPU_WORD # 4 entries never throttle the CPU
assert window_limit_mbs(4) < SINK_MBS # but would cap a streaming master below peak
assert window_limit_mbs(1) < CPU_LINE_FILL # a 1-deep FIFO would throttle the CPU
absorb = 16 * (1 - SINK_MBS / SRC_MBS) # entries to absorb a whole burst, not required
assert absorb == 8.0
print("PASS: 0.875 bursts in flight, 4 entries cap a stream near 219 MB/s but never throttle the CPU")Every crossing names its mechanism: FIFO depth with the throughput arithmetic, or handshake with the hold-stable rule.
The width table shows 32 bits on every link, so the review confirms conversion logic is absent rather than assumed correct.
Endianness is stated once for the chip, and every block datasheet records its agreement or its wrapper swizzle.
Drive back-to-back 16-beat line fills through the bridge with random stalls on valid and ready on both sides, and check with a scoreboard that no beat is dropped, duplicated or reordered: pointer-synchroniser and full or empty bugs show up only when both sides stall at awkward moments. Check that the FIFO never accepts a write while full (ready must be low) and never presents data while empty. Run byte, halfword and word accesses from software through any converter; a width bug corrupts neighbours on partial writes while full-word tests stay green.
They leave through different divider roots with separate clock trees, so the skew between them is uncontrolled. Signoff treats them as asynchronous and the crossing uses FIFOs either way.