The peak table says 384.0 MB/s per link, but the NanoSoC link table quietly quotes 361.4 sustained for 16-beat bursts. The missing 22.6 MB/s is not a slower slave or a narrower bus: it is one idle cycle per burst. NanoSoC's model charges that bubble once per burst whether or not the grant changes hands: a lone master pays it as address-to-data turnaround, alternating masters pay it as a switch. Any budget that multiplies peak by time without the bubble is wrong before simulation starts.
Round-robin grants masters in rotation, which guarantees no master waits longer than one rotation: starvation-free by construction. A burst of L beats costs L plus 1 cycles, so efficiency is L over L plus 1: four-beat cache-line fills run at 80 percent, sixteen-beat streams at 94.1, and single-beat register traffic at 50, all before any slave latency. Two details make the circuit correct. The last-granted bit must be updated on every grant, not only when both masters ask, or a master that won alone several times wins again the moment the other joins. And the grant must be held until the last beat of the granted burst (WLAST or RLAST), because AXI data cannot interleave bursts and the 17-cycle burst model forbids it. The module and testbench below do both, and the testbench fails if either rule is broken.
// NanoSoC teaching values: 2-master round-robin with a burst lock.
// The grant is held until the last beat of the granted burst, so bursts never
// interleave. The idle cycle of the link model is added by the fabric, not here.
module nano_rr_arb2 (
input logic clk,
input logic resetn,
input logic [1:0] req,
input logic burst_done, // last beat (WLAST or RLAST) of the granted burst accepted
output logic [1:0] grant
);
logic locked; // a burst is in progress: hold the grant
logic [1:0] owner; // who holds it
logic last; // 0: M0 was granted last, 1: M1 was granted last
always_comb begin
grant = 2'b00;
if (locked) grant = owner;
else case (req)
2'b01: grant = 2'b01;
2'b10: grant = 2'b10;
2'b11: grant = last ? 2'b01 : 2'b10;
default: grant = 2'b00;
endcase
end
always_ff @(posedge clk or negedge resetn) begin
if (!resetn) begin
locked <= 1'b0;
owner <= 2'b00;
last <= 1'b0;
end else if (!locked) begin
if (grant != 2'b00) begin
locked <= 1'b1;
owner <= grant;
last <= grant[1]; // updated on EVERY grant, not only when both ask
end
end else if (burst_done) begin
locked <= 1'b0;
end
end
endmodule
// ---- self-checking testbench: strict alternation, no interleaving ----
module tb;
logic clk = 0, resetn = 0;
always #5 clk = ~clk;
logic [1:0] req = 2'b00;
wire [1:0] grant;
logic [1:0] beat = 2'd0; // beat counter of the granted burst (4-beat bursts)
wire burst_done = (grant != 2'b00) && (beat == 2'd3);
nano_rr_arb2 dut (.clk(clk), .resetn(resetn), .req(req), .burst_done(burst_done), .grant(grant));
always @(posedge clk) begin
if (!resetn) beat <= 2'd0;
else if (grant != 2'b00) beat <= beat + 2'd1;
end
int errors = 0, n_m0 = 0, n_m1 = 0;
logic [1:0] owner = 2'b00, prev = 2'b00;
bit both_phase = 0, first = 1;
always @(posedge clk) if (resetn && grant != 2'b00) begin
if (beat == 2'd0) begin // a new burst starts
owner = grant;
if (both_phase) begin
if (first && grant != 2'b01) begin errors++; $display("FAIL: M0 must win after M1 went last"); end
if (!first && grant == prev) begin errors++; $display("FAIL: master %b granted twice in a row", grant); end
first = 0;
if (grant == 2'b01) n_m0++; else n_m1++;
end
prev = grant;
end else if (grant != owner) begin errors++; $display("FAIL: grant changed inside a burst"); end
end
initial begin
repeat (3) @(negedge clk); resetn = 1;
req = 2'b10; repeat (20) @(negedge clk); // M1 alone for several bursts
both_phase = 1; req = 2'b11; repeat (80) @(negedge clk); // then both ask continuously
req = 2'b00; repeat (6) @(negedge clk);
if (n_m0 < 8 || n_m1 < 8 || n_m0 > n_m1 + 1 || n_m1 > n_m0 + 1) begin errors++; $display("FAIL: unfair share m0=%0d m1=%0d", n_m0, n_m1); end
if (errors == 0) $display("PASS: strict alternation while both ask, no interleaving inside a burst (m0=%0d m1=%0d)", n_m0, n_m1);
$finish;
end
initial #50000 begin $display("FAIL timeout"); $finish; end
endmodule# NanoSoC teaching values: the burst bubble and the alternating CPU and SBR pair.
F = 96e6 # clk_sys
eff = {1: 1 / 2, 4: 4 / 5, 8: 8 / 9, 16: 16 / 17}
want = {1: 192.0, 4: 307.2, 8: 341.3, 16: 361.4}
for L, e in eff.items():
assert abs(384.0 * e - want[L]) < 0.1
# CPU 4-beat fills alternate with SBR 16-beat bursts on one bank: each burst pays one bubble
period_cycles = (4 + 1) + (16 + 1)
period_s = period_cycles / F
cpu_mbs = 16 / period_s / 1e6
sbr_mbs = 64 / period_s / 1e6
total_mbs = (16 + 64) / period_s / 1e6
assert abs(cpu_mbs - 69.8) < 0.1
assert abs(sbr_mbs - 279.3) < 0.1
assert abs(total_mbs - 349.1) < 0.1 # below the 361.4 of a pure 16-beat stream
# the worst wait for the CPU is one SBR burst plus its bubble
worst_wait_ns = (16 + 1) / F * 1e9
assert abs(worst_wait_ns - 177.1) < 0.1
print("PASS: burst-16 361.4, alternating pair 69.8 + 279.3 = 349.1 MB/s, CPU waits at most 177.1 ns")Every shared slave quotes sustained, not peak, with the burst length and bubble the number assumes.
The arbitration scheme per slave is named, with its worst-case wait bound written down (NanoSoC: one burst of the other master).
Single-beat traffic has its own budget row at 50 percent efficiency, never the burst number.
Alternate the two masters on one slave in simulation and measure each side against the 69.8 and 279.3 figures: a higher number means the bubble is missing from the model, a lower one means extra stalls to find. Then run each master alone and confirm both recover to their burst-efficiency rates; if they do not, the limit is in the slave, not the arbiter. For the arbiter itself, drive one master alone for several bursts and then both together: the first grant after the second master joins must go to it, and no burst may change owner mid-way.
Because 361.4 assumes 16-beat bursts, which is the SBR streaming pattern the SRAM budget is built for. CPU fills run at 307.2 on the wire and 128 end to end; each pattern gets its own row.