Under load the CPU instruction fetch and an SBR stream overlap in the fabric, and their read responses interleave on the way back. If the interconnect confuses the two IDs, the CPU executes stream data as instructions. This is the classic ID collision: data corruption under load that vanishes in directed tests, caught by ID-width lint and multi-master random tests, fixed by computing ID widths from the fabric instead of guessing them.
AXI tags every read address and write address with an ID, and three rules govern what comes back. First, read data returns in order within one ID but different IDs may interleave freely, so a master that needs order uses one ID and a master that wants parallelism uses several. Second, the interconnect must widen IDs with master-index bits so two masters using the same local ID stay distinct downstream, and strip those bits again on the return path to steer each response to its master. Third, AXI4 has no WID: write data must follow the order of the write addresses, and write interleaving was removed. AWID and BID remain, so the master still pairs each B response with its request. Two consequences for a bridge: write data may arrive before its address, so a bridge must not assume AW first, and a bridge that invents or drops write-ID bits is building a bug.
NanoSoC keeps this deliberately small: one ID value per master, INCR bursts up to 16 beats, no WRAP and no exclusive access. One ID value means per-ID ordering is automatic, and with two masters the slave side needs one master-index bit (three masters would need two). The interconnect is built here for 2-bit local IDs (an illustrative choice) so a future master with four IDs fits, giving 3-bit fabric IDs with the master bit on top.
What one ID value does not mean is one transaction at a time. A 16-beat read takes 4 clk_sys of fabric latency in this model (xbar address 1, memory read 2, xbar data 1). If the SBR issued the next address only after the last beat returned, it would sustain 384 x 16/21 = 292.6 MB/s, not the 361.4 MB/s of the link table. The table needs the next address issued while the previous burst is still returning data, so the SBR read engine keeps two bursts in flight on its one ID (an illustrative tracking depth, the minimum that reaches 361.4). The CPU is a blocking master with one access in flight, which is why its words cost 93.75 ns each (42.67 MB/s).
# NanoSoC teaching values: 2 masters, IDs widened at the fabric entry.
import math
LOCAL_BITS = 2 # illustrative: room for 4 local IDs per master
def fabric_id(master_idx, local_id):
# the interconnect puts the master index ABOVE the master's own ID bits
return (master_idx << LOCAL_BITS) | local_id
def route_back(fid):
# response path: the master bit steers the response, the rest is returned
return fid >> LOCAL_BITS, fid & ((1 << LOCAL_BITS) - 1)
all_ids = {fabric_id(m, l) for m in range(2) for l in range(4)}
assert len(all_ids) == 8, "two masters using the same local ID must stay distinct"
for m in range(2):
for l in range(4):
assert route_back(fabric_id(m, l)) == (m, l)
# the bug: a bridge that drops the master bit makes the two masters collide
assert len({fid & 3 for fid in all_ids}) == 4 < len(all_ids)
def per_id_in_order(beats):
# beats: (fabric_id, sequence number) in arrival order
last = {}
for fid, seq in beats:
if seq != last.get(fid, -1) + 1:
return False
last[fid] = seq
return True
# IDs may interleave, but the beats of one ID stay in order
good = [(0, 0), (4, 0), (0, 1), (4, 1), (0, 2), (4, 2)]
bad = [(0, 0), (0, 2), (0, 1), (4, 0)]
assert per_id_in_order(good)
assert not per_id_in_order(bad), "a reorder inside one ID must be caught"
# outstanding bursts the SBR needs to hold the sustained rate
LAT = 1 + 2 + 1 # xbar address, memory read, xbar data (clk_sys)
BURST = 16
one_at_a_time = 384.0 * BURST / (BURST + LAT + 1)
overlapped = 384.0 * BURST / (BURST + 1)
need = math.ceil((LAT + 1) / BURST) + 1
assert abs(one_at_a_time - 292.6) < 0.1 and abs(overlapped - 361.4) < 0.1
assert need == 2
print("PASS: 8 distinct fabric IDs, a dropped master bit is caught, 2 outstanding bursts give 361.4 not 292.6")The ID width at every port is derived from the master count plus the master-side width, and the lint rule forbids truncation.
The outstanding-transaction budget per master is written down (NanoSoC: the SBR needs two bursts in flight) and each slave interface tracks at least that many.
No bridge or converter carries a WID port or invents write ordering: write data follows write address order, B returns with BID, and AW is never assumed to arrive before W.
Run multi-master random traffic with response checking per ID: the reorder failure shows up only when two IDs interleave under arbitration pressure, never in directed tests. Assert outstanding counts at each slave interface and stress them; an overflow that drops or merges transactions is a limit mismatch between master and slave. If a response reaches the wrong master, read the ID remap table first: a dropped master-index bit is the usual cause. To confirm the two-burst rule, time one SBR read stream with the second address held back: the rate must fall from 361.4 to about 292.6 MB/s.
It does not have to: the interconnect adds a master-index bit above the local ID before the slave and strips it on return, so the two local ID 0s travel as distinct fabric IDs 0 and 4 and each response is steered back to the right master.