The stream runs at half rate with no error anywhere: 180.7 MB/s per direction instead of 361.4. The fabric is innocent, the DMA is innocent, the code is innocent. Source and destination sit in the same SRAM bank, and one single-port macro serves reads and writes together, so each direction gets half the port. Link sizing that stops at peak rates never sees it; only sustained rates per shared resource do.
Goal: the per-link table recomputed, and the bottleneck of the DMA stream and of CPU register traffic named from the table.
Inputs: 32-bit links, 96 MHz fabric, burst efficiency L/(L+1), the 64-word job formula N+8.
Steps: recompute peak and sustained per row; check the job time; compute the stream rate and its bottleneck for two banks and for one bank, and confirm the halving.
Link | Peak (MB/s) | Sustained (MB/s) |
|---|---|---|
Fabric per direction, 96 MHz | 384.0 | 361.4 at burst 16 |
Both SRAM banks together | 768.0 | 722.8 |
Same bank, per stream | 384.0 shared | 180.7 each |
Fast APB segment with bridge | 128.0 bus | 76.8 |
Slow APB segment through cdc | 48.0 bus | 19.2 |
CPU line fill, 24 clk_cpu | 768.0 | 128.0 |
MAP = [
("BOOTROM", 0x00000000, 0x4000),
("SRAM0", 0x20000000, 0x10000),
("SRAM1", 0x20010000, 0x10000),
("DMA", 0x40000000, 0x1000),
("SBR", 0x40001000, 0x1000),
("UART", 0x40002000, 0x1000),
("SPI", 0x40003000, 0x1000),
("GPIO", 0x40004000, 0x1000),
("INTC", 0x40005000, 0x1000),
("RSTPWR", 0x40006000, 0x1000),
]
SECURE_ONLY = {"SRAM1", "RSTPWR"}
SLOTS = ["DMA", "SBR", "UART", "SPI", "GPIO", "INTC", "RSTPWR"]
T_CPU, T_SYS, T_PER = 1e9 / 192e6, 1e9 / 96e6, 1e9 / 24e6
def end_of(base, size):
return base + size - 1
def overlaps(entries=MAP):
bad = []
xs = sorted(entries, key=lambda e: e[1])
for a, b in zip(xs, xs[1:]):
if b[1] < a[1] + a[2]:
bad.append((a[0], b[0]))
return bad
def misaligned(entries=MAP):
return [n for n, b, s in entries if b % s != 0]
def decode(addr, slots=SLOTS):
if addr >> 14 == 0:
return "BOOTROM"
if addr >> 16 == 0x2000:
return "SRAM0"
if addr >> 16 == 0x2001:
return "SRAM1"
if addr >> 16 == 0x4000:
slot = (addr >> 12) & 0xF
return slots[slot] if slot < 7 else "H3"
if addr < 0x20000000:
return "H1"
if addr < 0x40000000:
return "H2"
return "H4"
def response(addr, secure, slots=SLOTS):
t = decode(addr, slots)
if t in ("H1", "H2", "H4"):
return "DECERR"
if t == "H3":
return "SLVERR"
if t in SECURE_ONLY and not secure:
return "SLVERR"
return "OK"
def sustained(peak, beats):
return peak * beats / (beats + 1)
# ---- this lesson starts here ----
FABRIC = sustained(384.0, 16)
LINKS = {"cpu_fill": 16 / (24 * T_CPU) * 1e3, # one 16-byte line per 24 clk_cpu
"fabric": FABRIC, "sram_bank": FABRIC, # per direction; one bank port
"apb_fast": 4 * 96 / 5, # DMA and SBR registers, 3-clock access plus 2 bridge clocks
"apb_slow": 4 / 208.33e-9 / 1e6} # through nano_apb_cdc: 208.33 ns per access
def stream_rate(src_bank, dst_bank):
bank = LINKS["sram_bank"] / (2 if src_bank == dst_bank else 1) # one single-port macro serves both streams
path = {"sbr_read": LINKS["fabric"], "src_bank": bank, "dma_in": LINKS["fabric"],
"dma_out": LINKS["fabric"], "dst_bank": bank, "sbr_write": LINKS["fabric"]}
low = min(path.values())
return low, sorted(n for n, v in path.items() if v - low < 0.05)
assert abs(FABRIC - 361.4) < 0.05 and abs(LINKS["cpu_fill"] - 128.0) < 0.05
assert abs(LINKS["apb_fast"] - 76.8) < 0.01 and abs(LINKS["apb_slow"] - 19.2) < 0.05
assert abs(2 * FABRIC - 722.8) < 0.05
assert abs((64 + 8) * T_SYS - 750.0) < 0.5
rate, tied = stream_rate("SRAM0", "SRAM1")
assert abs(rate - 361.4) < 0.05 and len(tied) == 6 # six links tie: the 32-bit 96 MHz stream is the ceiling
rate, tied = stream_rate("SRAM0", "SRAM0")
assert abs(rate - 180.7) < 0.05 and tied == ["dst_bank", "src_bank"] # the shared bank port is the bottleneck
def placement_ok(src, dst):
return stream_rate(src, dst)[0] > 0.99 * FABRIC
assert placement_ok("SRAM0", "SRAM1") and not placement_ok("SRAM1", "SRAM1")
assert min(("apb_slow", LINKS["apb_slow"]), ("apb_fast", LINKS["apb_fast"]), key=lambda x: x[1])[0] == "apb_slow"
print("PASS: links agree: 361.4 per stream, 180.7 same-bank (bank port is the bottleneck), 64-word job 750 ns")One PASS line with the three numbers the lab quotes: full-rate stream, same-bank rate, job time. The two stream_rate calls are the failing-state demonstration: with the buffers in different banks six links tie at 361.4, so the stream itself is the ceiling; in one bank only the bank port drops, to 180.7, and the function names it. For CPU register traffic the slowest link is the 19.2 MB/s clk_per segment.
A 361.4 failure means the efficiency used the wrong burst length or forgot the one bubble per burst. A 19.2 failure means the cdc-plus-bridge total missed a handshake cycle: it is 2 plus 2 peripheral cycles and 2 plus 2 system cycles, 208.33 ns. A 750 ns failure means the 8-cycle fill and drain was dropped. A bottleneck list of more than the bank means the placement was already split across banks.
Hint 1: sustained is peak times beats over beats-plus-one; name the plus-one before computing. Hint 2: one single-port macro serves both directions, so divide, do not re-derive. Hint 3: the signature is a rate that is exactly half with no error flag anywhere, which a refusal would never show. Answer: put source in SRAM0 and destination in SRAM1, guard it with a buffer-placement rule checked at allocation, and budget 180.7 wherever sharing is unavoidable.
A 4-beat line costs 24 cpu clocks for 16 bytes: the issue, two crossings, fabric address, memory latency and response path dominate the four data beats.