Skip to content

Commit f405cf2

Browse files
committed
frontend: Ride stream_idx_o on the reg arbitration
stream_idx_o was driven by a last-wins scan over launch_pending, decoupled from the rr_arb_tree grant. With NumRegs>1 and multiple config ports launching different streams concurrently, the arbiter grants one port's dma_req_o while stream_idx_o points at another port's stream, so the transfer is tracked and completed under the wrong stream. Drive stream_idx_o from the arbiter's winning index (rr_arb_tree idx_o), gated by req_valid_o, so it always matches dma_req_o. NumRegs=1 was unaffected, which is why the existing tests missed it. Add a NumRegs=2 directed test: two ports launch on distinct streams, checking stream_idx_o matches the arbitrated port's stream. Fails pre-fix, passes after. Flagged by da-gazzi (#150).
1 parent 552919f commit f405cf2

3 files changed

Lines changed: 141 additions & 46 deletions

File tree

idma.mk

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -386,14 +386,16 @@ idma_sim_tb_idma_nd_midend_b2b: $(IDMA_VSIM_DIR)/compile.tcl
386386
cd $(IDMA_VSIM_DIR); $(VSIM) -c -do "source compile.tcl; quit"
387387
cd $(IDMA_VSIM_DIR); $(VSIM) -c -t 1ps -voptargs=+acc tb_idma_nd_midend_b2b -do "run -all; quit"
388388

389-
# Self-checking reg-frontend regression (NumStreams 1 and 2). Checks the
390-
# non-blocking next_id launch contract: bounded-latency read + no dropped launch.
389+
# Self-checking reg-frontend regression. Checks the non-blocking next_id launch
390+
# contract (bounded-latency read + no dropped launch) across NumStreams 1/2, and
391+
# the multi-port arbitration of stream_idx with NumRegs=2, NumStreams=2.
391392
# Run with the Questa SEPP wrapper.
392393
.PHONY: idma_sim_tb_idma_reg_frontend
393394
idma_sim_tb_idma_reg_frontend: $(IDMA_VSIM_DIR)/compile.tcl
394395
cd $(IDMA_VSIM_DIR); $(VSIM) -c -do "source compile.tcl; quit"
395396
cd $(IDMA_VSIM_DIR); $(VSIM) -c -t 1ps -voptargs=+acc -gNumStreams=1 tb_idma_reg_frontend -do "run -all; quit"
396397
cd $(IDMA_VSIM_DIR); $(VSIM) -c -t 1ps -voptargs=+acc -gNumStreams=2 tb_idma_reg_frontend -do "run -all; quit"
398+
cd $(IDMA_VSIM_DIR); $(VSIM) -c -t 1ps -voptargs=+acc -gNumStreams=2 -gNumRegs=2 tb_idma_reg_frontend -do "run -all; quit"
397399

398400
.PHONY: idma_sim_tb_idma_transpose_b2b
399401
idma_sim_tb_idma_transpose_b2b: $(IDMA_VSIM_DIR)/compile.tcl

src/frontend/reg/tpl/idma_reg.sv.tpl

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -98,20 +98,16 @@ module idma_${identifier} #(
9898
dma_req_t [NumRegs-1:0] arb_dma_req;
9999
logic [NumRegs-1:0] arb_valid;
100100
logic [NumRegs-1:0] arb_ready;
101+
// winning port index from the arbiter (drives stream_idx_o, see below)
102+
logic [cf_math_pkg::idx_width(NumRegs)-1:0] arb_idx;
101103

102104
// per-port launch bookkeeping (see gen_core_regs: launch_pending latch)
103105
logic [NumRegs-1:0] launch_pending;
104106
stream_t [NumRegs-1:0] held_stream;
105107

106-
// report the stream of the port whose launch is currently outstanding
107-
always_comb begin
108-
stream_idx_o = '0;
109-
for (int r = 0; r < NumRegs; r++) begin
110-
if (launch_pending[r]) begin
111-
stream_idx_o = held_stream[r];
112-
end
113-
end
114-
end
108+
// stream of the *arbitrated* port: must track dma_req_o (the arbiter winner), not the
109+
// last launch_pending port, else the transfer is tracked under the wrong stream.
110+
assign stream_idx_o = req_valid_o ? held_stream[arb_idx] : '0;
115111

116112
// generate the registers
117113
for (genvar i = 0; i < NumRegs; i++) begin : gen_core_regs
@@ -321,7 +317,7 @@ module idma_${identifier} #(
321317
.gnt_i ( req_ready_i ),
322318
.req_o ( req_valid_o ),
323319
.data_o ( dma_req_o ),
324-
.idx_o ( /* NC */ )
320+
.idx_o ( arb_idx )
325321
);
326322

327323
endmodule

test/tb_idma_reg_frontend.sv

Lines changed: 131 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616

1717
module tb_idma_reg_frontend import idma_pkg::*; #(
1818
// number of streams the elaborated DUT exposes (checked at instantiation)
19-
parameter int unsigned NumStreams = 32'd1
19+
parameter int unsigned NumStreams = 32'd1,
20+
// number of config-bus ports (arbitrated by the reg frontend's rr_arb_tree)
21+
parameter int unsigned NumRegs = 32'd1
2022
);
2123

2224
// --------------------------------------------------------------------------
@@ -27,7 +29,6 @@ module tb_idma_reg_frontend import idma_pkg::*; #(
2729
localparam int unsigned CfgDataWidth = 32'd32;
2830
localparam int unsigned CfgStrbWidth = CfgDataWidth / 32'd8;
2931
localparam int unsigned IdCounterWidth = 32'd32;
30-
localparam int unsigned NumRegs = 32'd1;
3132
// idma data-path (reg32_3d: 32-bit data, 3 ND dims)
3233
localparam int unsigned AddrWidth = 32'd32;
3334
localparam int unsigned DataWidth = 32'd32;
@@ -262,17 +263,25 @@ module tb_idma_reg_frontend import idma_pkg::*; #(
262263
else if (issue) launch_accept_count <= launch_accept_count + 1;
263264
end
264265

266+
// Test 5 (multi-port arbitration) scoreboard state. Each config port programs a
267+
// unique src_addr that encodes its target stream, so the arbiter's presented
268+
// winner (dma_req_o) can be mapped back to a stream and compared to stream_idx_o.
269+
logic [31:0] sb_addr_stream0; // src_addr programmed for stream 0's port
270+
logic [31:0] sb_addr_stream1; // src_addr programmed for stream 1's port
271+
int unsigned sb_mismatch; // times stream_idx != the winner's stream
272+
initial sb_mismatch = 0;
273+
265274
// --------------------------------------------------------------------------
266275
// APB driver
267276
// --------------------------------------------------------------------------
268-
task automatic apb_idle();
269-
apb_req[0].psel = 1'b0;
270-
apb_req[0].penable = 1'b0;
271-
apb_req[0].pwrite = 1'b0;
272-
apb_req[0].paddr = '0;
273-
apb_req[0].pwdata = '0;
274-
apb_req[0].pstrb = '0;
275-
apb_req[0].pprot = '0;
277+
task automatic apb_idle(input int unsigned port = 0);
278+
apb_req[port].psel = 1'b0;
279+
apb_req[port].penable = 1'b0;
280+
apb_req[port].pwrite = 1'b0;
281+
apb_req[port].paddr = '0;
282+
apb_req[port].pwdata = '0;
283+
apb_req[port].pstrb = '0;
284+
apb_req[port].pprot = '0;
276285
endtask
277286

278287
// Strict APB4 master. `access_cycles` (out) counts the ACCESS-phase config cycles
@@ -283,73 +292,77 @@ module tb_idma_reg_frontend import idma_pkg::*; #(
283292
input logic [31:0] wdata,
284293
output logic [31:0] rdata,
285294
output int unsigned access_cycles,
286-
input bit is_next_id = 1'b0);
295+
input bit is_next_id = 1'b0,
296+
input int unsigned port = 0);
287297
access_cycles = 0;
288298
// SETUP phase: psel high, penable low, for one cycle
289299
@(negedge clk);
290-
apb_req[0].psel = 1'b1;
291-
apb_req[0].penable = 1'b0;
292-
apb_req[0].pwrite = write;
293-
apb_req[0].paddr = addr;
294-
apb_req[0].pwdata = wdata;
295-
apb_req[0].pstrb = write ? '1 : '0;
296-
apb_req[0].pprot = '0;
300+
apb_req[port].psel = 1'b1;
301+
apb_req[port].penable = 1'b0;
302+
apb_req[port].pwrite = write;
303+
apb_req[port].paddr = addr;
304+
apb_req[port].pwdata = wdata;
305+
apb_req[port].pstrb = write ? '1 : '0;
306+
apb_req[port].pprot = '0;
297307
if (is_next_id) nxt_read_active = 1'b1;
298308

299309
// ACCESS phase: raise penable, then wait for pready at the posedge. Count the
300310
// ACCESS-phase cycles until pready — for a next_id read this is the read latency
301311
// the non-blocking contract bounds (must not depend on req_ready_i).
302312
@(negedge clk);
303-
apb_req[0].penable = 1'b1;
313+
apb_req[port].penable = 1'b1;
304314
forever begin
305315
@(posedge clk);
306316
access_cycles = access_cycles + 1;
307-
if (apb_rsp[0].pready) begin
308-
rdata = apb_rsp[0].prdata; // sample in the completing cycle
317+
if (apb_rsp[port].pready) begin
318+
rdata = apb_rsp[port].prdata; // sample in the completing cycle
309319
break;
310320
end
311321
end
312322
// retire the transaction: return to IDLE immediately (no extra held posedge)
313-
apb_idle();
323+
apb_idle(port);
314324
if (is_next_id) nxt_read_active = 1'b0;
315325
// mandatory idle cycle so the FSM's is_active fully drops with psel low
316326
@(negedge clk);
317327
endtask
318328

319-
task automatic apb_write(input logic [31:0] addr, input logic [31:0] data);
329+
task automatic apb_write(input logic [31:0] addr, input logic [31:0] data,
330+
input int unsigned port = 0);
320331
logic [31:0] dummy;
321332
int unsigned cyc;
322-
apb_xact(1'b1, addr, data, dummy, cyc);
333+
apb_xact(1'b1, addr, data, dummy, cyc, 1'b0, port);
323334
endtask
324335

325336
// APB4 read that respects pready; `cyc` returns the ACCESS-phase cycle count.
326337
task automatic apb_read(input logic [31:0] addr,
327338
output logic [31:0] data,
328339
output int unsigned cyc,
329-
input bit is_next_id = 1'b0);
330-
apb_xact(1'b0, addr, 32'h0, data, cyc, is_next_id);
340+
input bit is_next_id = 1'b0,
341+
input int unsigned port = 0);
342+
apb_xact(1'b0, addr, 32'h0, data, cyc, is_next_id, port);
331343
endtask
332344

333345
// --------------------------------------------------------------------------
334346
// High-level helpers
335347
// --------------------------------------------------------------------------
336348
task automatic program_transfer(input logic [31:0] src,
337349
input logic [31:0] dst,
338-
input logic [31:0] len);
350+
input logic [31:0] len,
351+
input int unsigned port = 0);
339352
// conf: plain 1D incremental copy, ND disabled
340-
apb_write(REG_CONF, 32'h0);
341-
apb_write(REG_SRC_ADDR, src);
342-
apb_write(REG_DST_ADDR, dst);
343-
apb_write(REG_LENGTH, len);
353+
apb_write(REG_CONF, 32'h0, port);
354+
apb_write(REG_SRC_ADDR, src, port);
355+
apb_write(REG_DST_ADDR, dst, port);
356+
apb_write(REG_LENGTH, len, port);
344357
endtask
345358

346359
// launch: read next_id (the transfer trigger, non-blocking); returns id and the
347360
// ACCESS-phase latency in `cyc`. Snapshots the accept count before the read so
348361
// an accept coinciding with the (non-blocking) read is still observed.
349362
task automatic launch(output logic [31:0] id, output int unsigned cyc,
350-
input int unsigned s = 0);
363+
input int unsigned s = 0, input int unsigned port = 0);
351364
launch_acc_base = launch_accept_count;
352-
apb_read(reg_next_id(s), id, cyc, .is_next_id(1'b1));
365+
apb_read(reg_next_id(s), id, cyc, .is_next_id(1'b1), .port(port));
353366
endtask
354367

355368
// wait until the launch read since the last launch() has been accepted (the
@@ -411,7 +424,7 @@ module tb_idma_reg_frontend import idma_pkg::*; #(
411424
initial begin : test
412425
errors = 0;
413426
checks = 0;
414-
apb_idle();
427+
for (int unsigned p = 0; p < NumRegs; p++) apb_idle(p);
415428
req_ready = 1'b1;
416429

417430
// reset
@@ -604,6 +617,90 @@ module tb_idma_reg_frontend import idma_pkg::*; #(
604617
$display("[ ok ] Test4 done_id advanced in order to %0d", ids[3]);
605618
end
606619

620+
// ------------------------------------------------------------------
621+
// Test 5 — Concurrent multi-port launch: stream_idx must ride the
622+
// arbitration (only meaningful when NumRegs>1 and NumStreams>1).
623+
// Two config ports launch on *different* streams in the same window; on
624+
// every grant stream_idx must match the ARBITRATED port's stream, not the
625+
// last-pending port. This FAILS on the pre-fix RTL and PASSES after it.
626+
// ------------------------------------------------------------------
627+
if (NumRegs > 1 && NumStreams > 1) begin
628+
logic [31:0] id_p0, id_p1;
629+
int unsigned cyc0, cyc1;
630+
$display("\n--- Test 5: concurrent multi-port arbitration (stream_idx) ---");
631+
backend_auto_retire = 1'b0;
632+
captured_q.delete();
633+
req_ready = 1'b0;
634+
// port 0 -> stream 0, port 1 -> stream 1, each with a unique src_addr
635+
sb_addr_stream0 = 32'hAAAA_0000;
636+
sb_addr_stream1 = 32'hBBBB_0000;
637+
program_transfer(sb_addr_stream0, 32'hCCCC_0000, 32'h0000_0040, 0);
638+
program_transfer(sb_addr_stream1, 32'hDDDD_0000, 32'h0000_0080, 1);
639+
sb_mismatch = 0;
640+
// launch both ports concurrently on different streams while the grant is held off,
641+
// so both launch_pending latches are set at once (the arbiter must pick one).
642+
fork
643+
launch(id_p0, cyc0, 0, 0); // port 0, stream 0
644+
launch(id_p1, cyc1, 1, 1); // port 1, stream 1
645+
join
646+
// Both launches are now pending with req_ready still low. rr_arb_tree (AxiVldRdy=1)
647+
// *presents* its chosen winner on dma_req_o / idx_o even while the grant is withheld,
648+
// so stream_idx_o must equal the winner's stream. On the pre-fix RTL stream_idx_o is
649+
// the last-pending port's stream (held_stream[NumRegs-1]) regardless of the winner,
650+
// so it disagrees with dma_req_o whenever the winner is not the last port. Check the
651+
// presented (winner, stream_idx) pair across the whole held window.
652+
begin
653+
int unsigned held_checks;
654+
held_checks = 0;
655+
repeat (12) begin
656+
@(negedge clk);
657+
#(TCK/10); // let combinational DUT outputs settle
658+
if (req_valid && !req_ready) begin
659+
automatic int unsigned won_stream = 32'hFFFF_FFFF;
660+
if (dma_req.burst_req.src_addr == sb_addr_stream0) won_stream = 0;
661+
else if (dma_req.burst_req.src_addr == sb_addr_stream1) won_stream = 1;
662+
if (won_stream != 32'hFFFF_FFFF) begin
663+
held_checks++;
664+
if (stream_idx != won_stream[$bits(stream_idx)-1:0]) begin
665+
sb_mismatch++;
666+
$display("[Test5] MISMATCH: dma_req_o=port for stream %0d but stream_idx=%0d",
667+
won_stream, stream_idx);
668+
end
669+
end
670+
end
671+
end
672+
check_eq(held_checks > 0, 1'b1, "Test5 winner presented while grant withheld");
673+
check_eq(sb_mismatch, 32'd0, "Test5 stream_idx matches arbitrated port (winner)");
674+
end
675+
// now let both launches drain and confirm both transfers are captured correctly
676+
req_ready = 1'b1;
677+
backend_auto_retire = 1'b1;
678+
begin
679+
int unsigned tries;
680+
tries = 0;
681+
while (captured_q.size() < 2) begin
682+
@(posedge clk);
683+
tries++;
684+
if (tries > 1000) $fatal(1, "Test5: both launches never drained (got %0d)",
685+
captured_q.size());
686+
end
687+
end
688+
check_eq(captured_q.size(), 32'd2, "Test5 both launches captured");
689+
begin
690+
logic saw_a, saw_b;
691+
saw_a = 1'b0; saw_b = 1'b0;
692+
foreach (captured_q[k]) begin
693+
if (captured_q[k].burst_req.src_addr == sb_addr_stream0) saw_a = 1'b1;
694+
if (captured_q[k].burst_req.src_addr == sb_addr_stream1) saw_b = 1'b1;
695+
end
696+
check_eq(saw_a, 1'b1, "Test5 port0/stream0 transfer captured");
697+
check_eq(saw_b, 1'b1, "Test5 port1/stream1 transfer captured");
698+
end
699+
$display("[ ok ] Test5 concurrent arbitration: %0d mismatches", sb_mismatch);
700+
end else begin
701+
$display("\n--- Test 5: skipped (needs NumRegs>1 and NumStreams>1) ---");
702+
end
703+
607704
// ------------------------------------------------------------------
608705
// Summary
609706
// ------------------------------------------------------------------

0 commit comments

Comments
 (0)