A method that proves the chip meets its bandwidth and latency budgets before tapeout needs traffic generators standing in for the missing neighbours and counters watching every link. Without it the table is a wish; with it a miss arrives with the link name attached. NanoSoC's block list has no performance monitors, so these are testbench monitors or monitors added for characterisation, and they are part of the verification plan, not of the chip.
The method has four steps. First, model: the two scripts in this topic predict every number. Second, generate: traffic generators replay the worst-case mix, CPU line fills against full-duplex SBR streaming, with bursts constrained to legal lengths and page edges. Third, count: monitors in the fabric count beats, bubbles, outstanding transactions and wait cycles per link over a window. Fourth, compare: each counter is checked against its model row, and any miss is read down to its link. The width of a counter is a separate sizing question: it must hold the most increments a window can produce, bits at least log2 of window cycles times increments per cycle, so a million-cycle window at one beat per cycle needs 20 bits. The script below models a window of 340 cycles on the SBR copy, with and without the shared-bank trap, and classifies what the counters show.
# NanoSoC teaching values: per-link beat counters over a 340-cycle window (20 bursts of 17 cycles).
import math
BURST = 16
WINDOW = 340
def run(shared_bank):
# One SBR copy: a read stream and a write stream, each always asking for 16-beat bursts.
# A bank port moves one beat per cycle and costs one bubble per burst.
write_link = "sram0_write" if shared_bank else "sram1_write"
counts = {"sram0_read": 0, write_link: 0}
if shared_bank:
t, turn = 0, 0
while t + BURST + 1 <= WINDOW: # one port: the two bursts alternate
counts["sram0_read" if turn == 0 else write_link] += BURST
t += BURST + 1
turn ^= 1
else:
for name in counts: # two ports: independent
t = 0
while t + BURST + 1 <= WINDOW:
counts[name] += BURST
t += BURST + 1
return counts
def classify(measured, model, errors=0):
low = {k: v / model[k] for k, v in measured.items() if v < 0.9 * model[k]}
if not low:
return "ok"
if errors and min(low.values()) == 0:
return "refusal"
if len(low) == len(measured) and all(abs(r - 0.5) < 0.05 for r in low.values()):
return "shared bank"
return "single link miss"
model = {"sram0_read": 320, "sram1_write": 320} # 20 bursts x 16 beats on each of two ports
model_shared = {"sram0_read": 320, "sram0_write": 320}
split, shared = run(False), run(True)
assert split == {"sram0_read": 320, "sram1_write": 320}
assert shared == {"sram0_read": 160, "sram0_write": 160} # both directions halved: 180.7 each
assert classify(split, model) == "ok"
assert classify(shared, model_shared) == "shared bank"
assert classify({"sram0_read": 320, "sram1_write": 150}, model) == "single link miss"
assert classify({"sram0_read": 0, "sram1_write": 0}, model, errors=3) == "refusal"
# counter width from the window, and a width that would overflow
window, per_cycle = 1_000_000, 1
bits = math.ceil(math.log2(window * per_cycle + 1))
assert bits == 20 and window * per_cycle > 2**16 - 1
print("PASS: split banks 320 + 320 beats, shared bank 160 + 160 classified as the shared-bank trap, 20-bit counters")The performance plan lists every budgeted link with its generator, its counters, and its pass threshold.
Counter width is derived from the longest window and the peak increment rate, and overflow is proved impossible, not assumed away.
A miss has a named owner and a drill: walk from the lowest count to the shared resource above it.
Run the worst-case mix with the counters free-running, then freeze and read: beats per link against the model rows, bubbles per burst against one, outstanding maxima against the tracking size. A link at partial beats with no error is contention; if both directions of one stream are halved together on one bank, it is the shared-bank trap at 180.7 per stream. A link at zero with errors is refusal, back at the firewall topic. A single link short while its partner is full is a different cause (a stall in that slave or path), so do not call it the shared-bank trap. The full flow this plugs into, with coverage and signoff, lives in the verification and bring-up section.
The SBR copy's two directions share bank 0, so each gets half the port, 180.7 MB/s: move the destination to SRAM1 and both recover. If only one link were short, the cause would lie in that link's own slave or path.