A DMA job, an instruction-fetch stream and a peripheral register poke each ask the fabric for something, and until each ask is a number in MB/s with a latency limit, the link table has nothing to answer. NanoSoC's workloads are small enough to price by hand, which is why the method sticks: bytes divided by time, plus the latency the software can tolerate.
Demand is bytes over time. The 64-word DMA job moves 256 bytes in 72 system clocks (750 ns): 64 beats plus 8 clocks of pipeline fill and drain, the NanoSoC specification's job model, which assumes one word per clock with no burst bubbles. That is 341.3 MB/s on average, inside the 361.4 sustained link rate, which is why the job fits. If the one bubble per 16-beat burst is counted, the four bursts need 64 + 4 = 68 read clocks plus the 8 of fill and drain: a 76-clock job (791.7 ns, 323.4 MB/s on average). The lesson uses the 72-clock model by default and says so whenever the 76-clock model changes an answer. The CPU line fill moves 16 bytes in 125 ns, 128 MB/s, with a 93.75 ns first-word latency the core stalls on. The SPI image load moves 8,196 bytes in 10,928 us, 0.75 MB/s: trivial for the fabric, binding on the SPI line. Each demand also carries a latency side: the fetch latency sets stall time, the interrupt latency (135.4 to 182.3 ns) sets response time, and the boot table's 12.581 ms total is 86.9% SPI load, which no fabric upgrade can shorten.
The script prices the three demands and checks each against its binding limit.
dma_bytes, dma_time = 64*4, 72 * (1/96e6)
dma_bw = dma_bytes / dma_time / 1e6
assert abs(dma_bw - 341.33) < 0.5, dma_bw
assert dma_bw < 361.4 # sustained link rate for burst 16
# Same job with one bubble per 16-beat burst counted: 4 bursts, 76 clocks.
clocks_bubbles = 64 + 64 // 16 + 8
assert clocks_bubbles == 76
bw_bubbles = dma_bytes / (clocks_bubbles / 96e6) / 1e6
assert abs(bw_bubbles - 323.4) < 0.2, bw_bubbles
# Pricing only the 64 beats gives 384 MB/s: above the 361.4 supply, a false overload.
bw_64 = dma_bytes / (64 / 96e6) / 1e6
assert abs(bw_64 - 384.0) < 0.01 and bw_64 > 361.4
# Averaging over the 72-clock window hides that the read channel is at peak rate.
assert bw_64 > dma_bw
fill_bw = 16 / 125e-9 / 1e6
assert abs(fill_bw - 128.0) < 0.5, fill_bw
spi_bw = 8196 / 10928e-6 / 1e6
assert abs(spi_bw - 0.75) < 0.01, spi_bw
boot_total = 12580.917e-6
assert abs(8196*8/6e6 / boot_total - 0.869) < 0.002
print(f"PASS: DMA {dma_bw:.1f}, fill {fill_bw:.0f}, SPI {spi_bw:.2f} MB/s; SPI is 86.9% of boot")Every workload priced as bytes over time with its time window stated.
Each demand paired with its latency limit: stall, response or boot share.
Demands summed per link before comparing with supply, not compared one by one.
Recheck the time window first when a demand looks wrong: the DMA's 750 ns is the job time including fill and drain (N + 8 clocks), not the 64 beat times. Two opposite errors are common. Pricing only the 64 beats (666.7 ns) gives 384 MB/s, which is above the 361.4 supply and falsely condemns a job that fits. The other error averages over the long 72-clock window and hides that the read channel runs at the full 384 MB/s peak rate during the 64 beats; a design whose supply is only just above the average has no room for that peak. Quote N + 8 for the average, and the beat-rate peak next to it.
Price latency beside every rate, because software feels the slower of the two. The DMA job's 341.3 MB/s looks healthy until the 8-cycle fill and drain is added for short jobs: a 4-word transfer moves 16 bytes in 12 clocks (125 ns), which is 128 MB/s, less than half the headline rate. The interrupt path's 135.4 to 182.3 ns bounds the response to every peripheral event. A demand table with rates but no latencies approves workloads that stall; add the stall, the response and the boot share to each row before comparing.
Because boot time is dominated by the slow line, not the fabric. The fabric idles between SPI bytes; speeding the fabric cannot shorten boot, only a faster line or a smaller image can.