A hang found in emulation costs weeks; the same hang found by a dependency review costs an afternoon. The integrator's prevention is structural, and the debug is mechanical. When a chip does freeze, check clocks and resets first: it takes a minute and rules out the commonest cause. Only then is the hang a dependency problem, and this lesson gives the rules and the method for that case.
A path is provably free of dependency cycles when every wait in it is allowed by the AXI handshake rules and the chain of waits ends at a component that always answers. The public rules are short. A master must not wait for AWREADY, WREADY or ARREADY before asserting AWVALID, WVALID or ARVALID. A slave may wait for AWVALID or WVALID (or both) before asserting AWREADY or WREADY. A slave must wait for both address and data handshakes (up to WLAST) before asserting BVALID, and for the address handshake before RVALID. A slave must not wait for BREADY or RREADY before asserting BVALID or RVALID; a master may wait for BVALID or RVALID before asserting BREADY or RREADY. Together they give a one-way order: AR before R, AW and W before B, and no edge back from a response to a request.
The integrator's job is to check that each component added to the fabric obeys them and adds no edge of its own. Walk the NanoSoC write to a UART register: the CPU waits for B; the axi_cdc passes it through five independent FIFOs; the xbar routes it; the axi2apb bridge accepts one access at a time and waits for PREADY; the apb_cdc runs one request and acknowledge handshake; the UART answers with zero wait states. The only places that serialise are axi2apb and apb_cdc, and both wait on an APB slave that always answers (PREADY within a wait state, or the 256-cycle watchdog), and no APB slave ever waits on the fabric. That is the freedom argument: the chain of waits ends, and no wait points back.
# not-run: NanoSoC deadlock-prevention checklist, NanoSoC teaching values
1. axi_cdc: independent, five channel FIFOs (AW, W, AR, B, R).
axi2apb and apb_cdc: one access at a time by design. Safe because the only
thing they wait on is an APB slave that always answers (PREADY or the
256-cycle watchdog), and no APB slave ever waits on the fabric.
2. Shared buffers: 4-deep FIFOs per AXI channel, one APB beat at a time. A bounded
buffer is full eventually either way; freedom comes from item 1, not from depth.
3. Power-down mid-transaction refused: gate blocked while BUSY, watchdog answers SLVERR.
4. Reset joins bridges: rst_sys_n and rst_cpu_n asserted together, released slaves first.
5. Default slaves answer every address: no waiter without a responder.# NanoSoC teaching values: the wait chain of a CPU write to a UART register.
# An edge a -> b means a cannot complete until b does.
CHAIN = {
"cpu_write": ["axi_cdc_aw_w_fifos"],
"axi_cdc_aw_w_fifos": ["xbar_route"],
"xbar_route": ["axi2apb_access"],
"axi2apb_access": ["apb_cdc_handshake", "watchdog"],
"apb_cdc_handshake": ["uart_pready"],
"uart_pready": [], # zero wait states on clk_per slaves: always answers
"watchdog": [], # 256 clk_sys cycles, then SLVERR
}
def acyclic(graph):
state = {}
def visit(n):
if state.get(n) == 1:
return False
if state.get(n) == 2:
return True
state[n] = 1
ok = all(visit(m) for m in graph.get(n, []))
state[n] = 2
return ok
return all(visit(n) for n in graph)
def every_wait_ends(graph, start):
# follow the waits: they must reach a node with no further wait
seen, stack = set(), [start]
while stack:
n = stack.pop()
if n in seen:
continue
seen.add(n)
stack.extend(graph.get(n, []))
return any(not graph.get(n) for n in seen)
assert acyclic(CHAIN) and every_wait_ends(CHAIN, "cpu_write")
# inject the property that must never exist: an APB slave that waits on the fabric
bad = dict(CHAIN)
bad["uart_pready"] = ["cpu_write"]
assert not acyclic(bad), "a slave waiting on the fabric must be caught"
# and a bridge without its watchdog and a slave that never answers has no end
dead = {"cpu_write": ["axi2apb_access"], "axi2apb_access": ["dead_slave"], "dead_slave": ["dead_slave"]}
assert not acyclic(dead)
print("PASS: NanoSoC write chain is acyclic and ends at an always-answering slave; two injected faults are caught")The channel-dependency review is a signed artefact, not a hallway conversation, listing each bridge, what it serialises and where its chain of waits ends.
Formal connectivity and liveness checks cover illegal-address and secure-fault traffic, not just the happy path.
Power-down and reset sequences list their quiesce steps with the transactions that must drain first.
When the fabric freezes, work the triage in order: confirm clocks and resets are good (a minute, and the commonest cause), then dump outstanding transactions per master and per bridge. Draw the waiter ring; break it by completing one member by hand; the member whose completion unwinds the rest names the fix. Escalate from simulation to emulation only with the traffic mix that reproduces it, and carry the same dump-and-ring method there. For the hang-debugging craft see the hardware-debug course; for coherent-fabric variants see the virtual-network lesson it links.
Power was cut with transactions in flight, so a live master waits on a response from a dead slave. The NanoSoC power-down order exists to prevent this: software quiesces the DMA and SBR, the clamp is asserted, the clocks are gated, resets assert, and only then does the power switch open.