The DMA datasheet rate of one word per clock is true at its stream port and false everywhere else: the source SRAM reads at 361.4 MB/s sustained, both streams share one port when they land in one bank, and the APB segment moves 76.8 MB/s with its bridge and wait states. A test that only checks the stream port certifies a number the system can never use. Its one job is the bandwidth method: which rate is proved on which link, with what traffic, and which efficiency term explains each gap between peak and sustained.
Every link has three numbers: peak from width times clock, sustained with the burst efficiency, and measured under traffic. Peak is 4 bytes times 96 MHz, 384.0 MB/s, on every 32-bit fabric link that runs at 96 MHz (the CPU side of the clock-domain bridge runs at 192 MHz and peaks at 768 MB/s). Sustained multiplies by the efficiency for the burst length: a 16-beat burst with one bubble cycle per burst runs at 16/17, giving 361.4 MB/s. The traffic generator proves it by issuing back-to-back 16-beat bursts from the SBR and counting bytes over a fixed window, first with source and destination in different banks, then in the same bank. The different-bank run needs the SBR to be secure (CTRL.NS = 0), because SRAM1 is secure-only and a non-secure SBR gets SLVERR at the first beat (planted bug 7). Different banks must show the full 361.4 each way; the same bank must show 180.7 each, the halving that the bandwidth table prices and the standing trap of putting a stream's source and destination in one bank.
Three more links complete the picture. The APB path to the DMA and SBR proves 76.8 MB/s sustained — five system clocks per 4-byte access with the bridge and one wait state — while the slow segment behind the clock-domain bridge proves 19.2 MB/s. The UART line proves 0.0115 MB/s at 115,384.6 baud, which matters because the banner alone consumes 11 percent of boot. Arbitration gets two tests. With the CPU line-filling from the ROM while the SBR streams SRAM0 to SRAM1, the masters share no slave and each keeps its rate. With the CPU line-filling from SRAM0 while the SBR reads SRAM0, they share one port: the two rates must sum to no more than the 361.4 MB/s of that port (the figure that assumes one bubble per 16-beat burst), and the round-robin change-of-master bubble of one idle cycle is the only loss permitted beyond that sharing. For the alternating pattern of a 4-beat CPU fill and a 16-beat SBR burst there are two bubbles per 20 beats, so the bound is 384 x 20/22 = 349.1 MB/s (20 beats in 22 cycles), not 361.4.
# Bandwidth check: a single memory port serves bursts to the streams that want it,
# round robin, one idle cycle per grant (the NanoSoC arbitration rule). The halving
# of the same-bank run is MEASURED from the grants, not typed in. NanoSoC teaching values.
W, F_MHZ = 4, 96
def run(streams, ports, bubbles=1, cycles=7480):
"""streams: name -> burst length (beats); ports: list of lists of stream names.
Returns MB/s per stream over the window, simulated cycle by cycle."""
beats = {name: 0 for name in streams}
for names in ports: # each port is independent
grant, left, idle, nxt = None, 0, 0, 0
for _ in range(cycles):
if idle:
idle -= 1
elif grant is None: # arbitrate: next stream in round-robin order
grant, left = names[nxt % len(names)], streams[names[nxt % len(names)]]
nxt += 1
if grant is not None and not idle:
beats[grant] += 1
left -= 1
if left == 0:
grant, idle = None, bubbles
return {n: b * W / (cycles / F_MHZ) for n, b in beats.items()}
peak = W * F_MHZ
budget = peak * 16 / 17
assert abs(peak - 384.0) < 0.01 and abs(budget - 361.4) < 0.1
# SBR read and write streams, each its own 16-beat bursts
split = run({"rd": 16, "wr": 16}, [["rd"], ["wr"]]) # source and destination in different banks
shared = run({"rd": 16, "wr": 16}, [["rd", "wr"]]) # same bank: one port serves both
assert all(abs(v - budget) < 0.5 for v in split.values()), split
assert all(abs(v - budget / 2) < 0.5 for v in shared.values()), shared # 180.7 each
# arbitration: a 4-beat CPU fill alternating with a 16-beat SBR burst on one port
mixed = run({"cpu": 4, "sbr": 16}, [["cpu", "sbr"]])
total = sum(mixed.values())
assert abs(total - peak * 20 / 22) < 0.5 and abs(total - 349.1) < 0.2, total
assert total < budget # below the 361.4 single-stream figure
# the check can fail: a second bubble per grant (an arbitration fault) is caught
slow = run({"rd": 16, "wr": 16}, [["rd"], ["wr"]], bubbles=2)
assert all(v < budget - 10 for v in slow.values())
apb_fast = W * F_MHZ / 5 # bridge 2 clocks plus 3 APB clocks per access
uart_mbs = 115384.6 / 10 / 1e6 # 10 line bits per byte
assert abs(apb_fast - 76.8) < 0.1 and abs(uart_mbs - 0.01154) < 0.0001
print(f"peak {peak:.1f}, different banks {split['rd']:.1f}, same bank {shared['rd']:.1f}, cpu+sbr {total:.1f}")
print("PASS: fabric 361.4, same bank 180.7 each (measured), 349.1 for 4+16, two-bubble fault caught, APB 76.8 MB/s")Each link names its traffic shape — burst length, bank placement, competing master — not just its number.
The same-bank run is a required line, with the 180.7 MB/s expectation written before the run.
Arbitration is proved under cross-traffic: the bound is 361.4 MB/s at most for one stream, and about 349 MB/s for the alternating 4-beat and 16-beat pattern, with the one-cycle bubble per grant as the only loss.
A link that misses its sustained rate is debugged by efficiency term: count the bubble cycles per burst first — arbitration, address turnaround, bridge accept — then check bank placement, then burst length. A short burst at length 4 runs at 80 percent before anything is wrong; the table says so. If the rate varies run to run, the traffic is not back-to-back and the generator, not the fabric, is the suspect.
The single SRAM port: reads and writes share one access per clock, so the two streams split the 361.4 MB/s of the port. Move the destination to SRAM1, which is secure-only, so the SBR must then be secure (CTRL.NS = 0).