The first symptom that points at the topology is a stream that runs at half speed for no visible reason. On NanoSoC the SBR-to-SRAM stream sustains 361.4 MB/s when source and destination sit in different banks, and 180.7 MB/s when they share one. Nothing is broken in the slow case: two masters are simply asking one single-port memory for data at once, and the fabric serialises them. Whether that serialisation happens everywhere or only at shared slaves is the difference between a shared bus and a crossbar, and it is decided before any RTL is written.
A shared bus lets one transaction through at a time: every master arbitrates for the same wires, so the aggregate peak is the bus peak no matter how many masters are idle. A crossbar gives each slave its own arbiter and multiplexer, so masters talking to different slaves proceed in parallel and only collide when they want the same slave in the same cycle. The price is wiring and area that grow with masters times slaves, plus one arbiter per slave that must close timing every cycle.
NanoSoC is a 2-master by 4-slave AXI4 crossbar: M0 is the CPU through its clock-domain bridge, M1 is the SBR, and the slaves are ROM, SRAM0, SRAM1 and the APB bridge. Behind that bridge sits a second level, an APB tree with a 12-bit address, one 96 MHz segment for the DMA and SBR registers and one 24 MHz segment behind the apb_cdc for the five slow peripherals. Hierarchy is the third idea: fast masters share a wide fast fabric, slow registers share a narrow slow bus, and a bridge with its own clock and power rules joins them. Memory traffic from the CPU and the SBR never queues behind a UART access, and the UART logic never pays for a 96 MHz clock tree. The one place accesses do queue is the bridge itself: S3 handles one access at a time, so a UART access in flight holds off any other access to the APB window, including an SBR register write, and the CPU itself waits 281.25 ns for each UART access because it has one access in flight.
Peak on any 32-bit 96 MHz link is 4 bytes times 96 MHz, 384.0 MB/s. With 16-beat bursts and one arbitration bubble per burst the sustained rate is 384 times 16/17, 361.4 MB/s. The script below checks the NanoSoC table, then compares split traffic against shared-bank traffic: two banks give twice 361.4, one shared port gives half of 361.4 to each stream.
W = 4 # bytes per beat on the 32-bit fabric
F = 96.0 # MHz on clk_sys
PEAK = W * F # 384.0 MB/s
def sustained(burst):
return PEAK * burst / (burst + 1)
assert abs(PEAK - 384.0) < 1e-9
assert abs(sustained(1) - 192.0) < 0.05
assert abs(sustained(4) - 307.2) < 0.05
assert abs(sustained(16) - 361.4) < 0.05
split_total = 2 * sustained(16)
shared_each = sustained(16) / 2
assert abs(split_total - 722.8) < 0.1
assert abs(shared_each - 180.7) < 0.05
print("PASS: peak 384.0, burst-16 361.4, split 722.8 total, shared 180.7 each")The topology paragraph names every master, every slave, and which pairs may proceed in parallel.
Each slave lists its worst-case concurrent demand, and the shared slaves show the sharing arithmetic above.
Slow peripherals sit behind a bridge, never on the fast fabric, with the latency budget that choice implies.
Recheck the sharing arithmetic whenever a buffer moves: the 180.7 MB/s shared-bank trap fires exactly when the SBR's source and destination land in the same SRAM bank, because its read stream and its write stream then share one single-port memory. In simulation, run an SBR copy with source and destination in the same bank and confirm each direction sees about half the port rate; then move the destination to the other bank and confirm both recover to the full 361.4 MB/s. A CPU line-fill pattern competing with the SBR for one bank is a different case, the 69.8 and 279.3 MB/s split of the arbitration topic. For the bridge, issue a UART access and an SBR register write back to back and confirm the second waits for the first.
Split across banks each sees the full burst-16 rate, 361.4 MB/s. Sharing one single-port bank they split the port, about 180.7 MB/s each.