Demands summed per link meet the sustained supply of that link, and the tightest link names the bottleneck. NanoSoC hides two textbook cases: source and destination in the same SRAM bank halve each stream to 180.7 MB/s, and a workload mix that sums past the achievable rate misses its target no matter how each master looks alone.
Walk every link the workload touches and divide the demand that lands on it by its sustained supply: the highest ratio is the bottleneck, and anything above 1.0 fails. The unit of comparison is the port, not the master. The DMA job at 341.3 MB/s against 361.4 is at 94% when the read stream sits on one SRAM port and the write stream on the other: fits, with little room. Put the CPU's instruction-fill traffic (128 MB/s) on the same port as the DMA read and that port carries 469.3 MB/s against 361.4: a ratio of 1.30, so the job stretches. Put source and destination in the same bank and one single-port SRAM carries both streams, 2 x 341.3 = 682.7 MB/s against 361.4: a ratio of 1.89, which is the same as each stream getting 180.7 MB/s against its 341.3 demand. Add the CPU fill to that same port and the demand is 810.7 MB/s against 361.4: a ratio of 2.24, so the port delivers only 45% of the ask. The fix order is fixed: place buffers on separate banks, shorten bursts or arbitrate, and only then widen the link.
The script runs the bottleneck check on three placements and proves which fails and by how much.
dma, fill, port = 341.3, 128.0, 361.4 # MB/s; port = sustained rate of one SRAM port
# Demand that lands on the busiest SRAM port in each placement.
cases = {"split banks": dma,
"split banks, CPU fills from the DMA source bank": dma + fill,
"same bank (both streams on one port)": 2 * dma,
"same bank plus CPU fill": 2 * dma + fill}
ratios = {}
for name, demand in cases.items():
ratios[name] = demand / port
print(f" {name}: demand {demand:.1f} vs supply {port:.1f} = {ratios[name]:.2f} "
f"{'OK' if ratios[name] <= 1.0 else 'BOTTLENECK'}")
assert abs(ratios["split banks"] - 0.944) < 0.001 and ratios["split banks"] <= 1.0
assert abs(ratios["split banks, CPU fills from the DMA source bank"] - 1.30) < 0.005
assert abs(ratios["same bank (both streams on one port)"] - 1.889) < 0.005
assert abs(dma / (port / 2) - 1.889) < 0.005 # same number as 341.3 against 180.7 per stream
assert abs(ratios["same bank plus CPU fill"] - 2.24) < 0.005
assert abs(port / (2 * dma + fill) - 0.446) < 0.002 # the port delivers 45% of the ask
# The check can fail: the worst placement must be the one flagged as the bottleneck.
worst = max(ratios, key=ratios.get)
assert worst == "same bank plus CPU fill" and ratios[worst] > 1.0
print("PASS: split fits at 0.94; CPU on the DMA bank 1.30; same bank 1.89; same bank plus CPU 2.24")One demand-over-supply ratio per touched link, with the bottleneck named and its ratio quoted.
Buffer placement stated as a rule (DMA buffers in the bank the CPU is not fetching from), not as advice.
Every ratio above 0.9 carries a mitigation: placement, arbitration or scope cut.
When silicon runs slower than the budget, check placement before rates: read back the actual source and destination addresses and confirm they sit in different banks. The same-bank trap is a software placement bug with a hardware symptom, and no link upgrade fixes it. Then check the burst lengths the master really issued — short bursts cut the efficiency the table assumed (burst length 1 halves it).
Confirm the bottleneck with measurement, not only arithmetic: fabric performance monitors count beats per master per window, and the losing master shows stalled READY or longer grant waits while the winner saturates. If arbitration is round-robin with a one-cycle change penalty, the fix for a 0.94 ratio may be arbitration tuning or burst-length discipline rather than placement — but placement comes first because it costs software nothing. Re-run the ratio after each fix; a bottleneck that moves to the next link is progress, and the table says which link is next.
Demand is 2 x 341.3 + 128 = 810.7 MB/s against 361.4 MB/s on the single port: a ratio of 2.24. The port delivers 45% of the ask. Move the DMA destination to SRAM1 first (the demand on each port then falls to 469.3 and 341.3, ratios 1.30 and 0.94), then move the CPU's fill traffic off the DMA source bank.