The bug that belongs to this lesson hangs the whole chip with no error and no timeout: software writes CLK_EN to zero to save power, the DMA clock stops mid-idle, and the next APB access never completes because the slave cannot raise pready. Only a reset recovers. The bridge did nothing wrong; it was placed, clocked and gated as if it were a wire. This lesson prices each NanoSoC bridge so that no one treats them as wires again.
Every bridge costs three things. Latency: the axi_cdc adds 3 clk_sys cycles on the request path and 3 clk_cpu cycles on the response path, plus address and data cycles, which is why a CPU word from SRAM costs 93.75 ns while the SRAM itself answers in two cycles. Throughput: the APB segments top out at 76.8 and 19.2 MB/s, so bulk movement must never cross them. And control coupling: a bridge joins two clock domains, two reset domains and sometimes two power domains, so gating or resetting one side without quiescing the other hangs the survivor.
# not-run: NanoSoC bridge cost table, NanoSoC teaching values
CPU word from SRAM: 7 T_sys + 4 T_cpu = 72.92 + 20.83 = 93.75 ns (42.67 MB/s)
CPU register, fast APB: 10 T_sys + 4 T_cpu = 104.17 + 20.83 = 125.00 ns
CPU register, slow APB: 9 T_sys + 4 T_per + 4 T_cpu = 93.75 + 166.67 + 20.83 = 281.25 ns
apb_cdc + bridge alone: 2 T_per + 2 T_per + 2 T_sys + 2 T_sys = 208.33 ns (19.2 MB/s)
axi_cdc each way: 3 T_sys request, 3 T_cpu responseThree placement rules follow. First, bridges sit on domain boundaries: axi_cdc where 192 MHz meets 96 MHz, axi2apb where the fast fabric meets registers, apb_cdc where 96 MHz meets 24 MHz and the always-on domain. Second, bridges are reset with both sides together and released slaves-first: the CPU leaves reset last, eight system clocks plus two CPU clocks after the fabric, so no fetch enters a bridge that is still in reset. Third, a bridge clock is never gated while its block may be addressed: the hardware refuses to gate a BUSY block, and the axi2apb carries a 256-cycle watchdog that completes a hung access with an error instead of hanging forever. The sizing rule is that the watchdog must be longer than the slowest legal access: the slowest access the bridge itself waits for is the clk_per access of 208.33 ns, 20 clk_sys cycles, and 256 cycles (2.67 us) is 12.8 times that (9.5 times the 27 clk_sys cycles the CPU sees for the same register).
T_SYS = 1 / 96e6 * 1e9 # 10.4167 ns
T_CPU = 1 / 192e6 * 1e9 # 5.2083 ns
T_PER = 1 / 24e6 * 1e9 # 41.6667 ns
cpu_sram = 7 * T_SYS + 4 * T_CPU
cpu_fast = 10 * T_SYS + 4 * T_CPU
cpu_slow = 9 * T_SYS + 4 * T_PER + 4 * T_CPU
cdc_bridge = 2 * T_PER + 2 * T_PER + 2 * T_SYS + 2 * T_SYS
assert abs(cpu_sram - 93.75) < 0.01
assert abs(cpu_fast - 125.0) < 0.01
assert abs(cpu_slow - 281.25) < 0.01
assert abs(cdc_bridge - 208.33) < 0.01
assert abs(4 / (cdc_bridge * 1e-9) / 1e6 - 19.2) < 0.05
print("PASS: 93.75, 125.00, 281.25 and 208.33 ns bridge costs recomputed")Every bridge has a row in the latency budget with its accept, crossing and respond cycles named.
Clock enables driven by CLK_EN list the quiesce rule: no gate while BUSY, watchdog present on the APB path.
The reset plan releases the CPU last, with the 8 plus 2 cycle stagger written as an assertion, not a comment.
Gate the DMA clock in simulation, then access a DMA register, and confirm the watchdog answers with SLVERR after 256 clk_sys cycles instead of hanging; then try to gate a BUSY block and confirm the refusal logic blocks the gate. Release resets in the wrong order on purpose and confirm the reset-order assertion fires before the first fetch is lost. Both checks replay real NanoSoC integration bugs: the gated-clock hang and the early-CPU release.
The stream stalls mid-transfer and the job never completes. This is the other half from the pitfall: the refusal to gate a BUSY block is missing, so the driver must idle the job first and the gate must check it. The watchdog does not help here, because no APB access is waiting.