On a non-coherent fabric a read goes to memory and returns; on a coherent fabric a cacheable read first asks every other cache whether it holds the line, because a dirty copy elsewhere is newer than memory. That question and its answers are extra transactions on the same wires the data uses, and they change bandwidth, latency and deadlock analysis at once. NanoSoC hides the problem today because its DMA buffers live in SRAM1, which no cache ever holds, so software never has to ask. A hook for later: that placement is exactly what stops working when a buffer must become cacheable.
Coherent masters speak an extended protocol, ACE on the AXI family, with extra snoop channels: a read that misses queries the other caches, and a write to a shared line invalidates or updates their copies. Two organisations answer the query. Snooping broadcasts to every other cache and collects the answers: simple, with traffic that grows with every cache added. A directory keeps a record of who holds each line and queries only the sharers: bookkeeping per line, with traffic that scales. The two are compared in the architecture course (snoop versus directory); the integrator's question is the count. A 64-word NanoSoC job is 256 bytes, 16 lines of 16 bytes, so every pass over it costs 16 line queries per other cache under broadcast and 16 times the number of sharers under a directory. Either way the interconnect carries three traffic classes now, requests, snoops and responses, and the separation rule of the deadlock topic applies to all three pairs.
# Illustrative: snoop queries for one 64-word (256-byte) NanoSoC job, 16-byte lines.
LINE = 16
JOB_BYTES = 64 * 4
lines = JOB_BYTES // LINE
assert lines == 16
def snoops_broadcast(n_caches, lines):
# a read miss asks every OTHER cache
return lines * (n_caches - 1)
def snoops_directory(sharers, lines):
# the directory asks only the caches that hold the line
return lines * sharers
assert snoops_broadcast(2, lines) == 16 and snoops_directory(1, lines) == 16 # equal at 2 caches
assert snoops_broadcast(4, lines) == 48 and snoops_broadcast(8, lines) == 112
assert snoops_directory(1, lines) == 16 # directory traffic does not grow with cache count
# snoop budget row: a master streaming at 361.4 MB/s, one 8-byte query and answer per line
lines_per_s = 361.4e6 / LINE
snoop_mbs = lines_per_s * 8 / 1e6
assert abs(snoop_mbs - 180.7) < 0.1 # half a link's worth of side traffic: it needs a row
print("PASS: 16 lines per job, broadcast 16 / 48 / 112 snoops at 2 / 4 / 8 caches, directory 16, snoop row 180.7 MB/s")Every master is labelled coherent or non-coherent in the map, with the coherent set justified, not inherited.
Snoop bandwidth has a budget row beside data bandwidth on every shared link, from the sharing pattern (lines per second times bytes per query).
Snoop and response paths are shown to drain independently, per the deadlock topic.
Drive coherent and non-coherent masters together with overlapping addresses and check data against a single-writer model: the failure is a stale read that passes every non-overlapping test. Measure snoop traffic per link with counters and compare against the budget row; an over-budget snoop rate means the sharing pattern, not the protocol, needs redesign.
Snoop traffic per read, because every query now reaches one more cache (a 16-line job goes from 16 to 32 queries); worst snoop latency, bounded by the slowest of the responders; and the deadlock review, which gains a new requester in the dependency graph.