// SPDX-License-Identifier: Apache-2.0 // Replays a session recorded from the host's software model in its AXI form (`gen-tb --axi`) // against the AXI link (rtl/landscape_axi.v) and the engine behind it: register accesses over // AXI-Lite, write lists through the DMA from a memory model that answers with irregular delays and // several bursts outstanding, and batches of chains through the sequencer, whose results are read // from the result RAM and written back to the memory model. Every read and every word written back // must be what the model returned. A mismatch names the line. `timescale 1ns/1ps module tb; reg clk = 0; always #5 clk = ~clk; // pl_clk0, 100 MHz reg rstn = 0; // AXI-Lite master reg [31:0] awaddr = 0, wdata = 0, araddr = 0; reg awvalid = 0, wvalid = 0, bready = 0, arvalid = 0, rready = 0; wire awready, wready, bvalid, arready, rvalid; wire [1:0] bresp, rresp; wire [31:0] rdata; // DMA read port wire [48:0] m_araddr; wire [7:0] m_arlen; wire [2:0] m_arsize, m_arprot; wire [1:0] m_arburst; wire [3:0] m_arcache; wire m_arvalid, m_rready; reg m_arready = 0, m_rvalid = 0, m_rlast = 0; reg [63:0] m_rdata = 0; reg [1:0] m_rresp = 0; // DMA write port wire [48:0] m_awaddr; wire [7:0] m_awlen, m_wstrb; wire [2:0] m_awsize, m_awprot; wire [1:0] m_awburst; wire [3:0] m_awcache; wire [63:0] m_wdata; wire m_awvalid, m_wlast, m_wvalid, m_bready; reg m_awready = 0, m_wready = 0, m_bvalid = 0; reg [1:0] m_bresp = 0; wire [94:0] emio_i; landscape_axi dut ( .clk(clk), .rstn(rstn), .s_awaddr(awaddr), .s_awvalid(awvalid), .s_awready(awready), .s_wdata(wdata), .s_wvalid(wvalid), .s_wready(wready), .s_bresp(bresp), .s_bvalid(bvalid), .s_bready(bready), .s_araddr(araddr), .s_arvalid(arvalid), .s_arready(arready), .s_rdata(rdata), .s_rresp(rresp), .s_rvalid(rvalid), .s_rready(rready), .m_araddr(m_araddr), .m_arlen(m_arlen), .m_arsize(m_arsize), .m_arburst(m_arburst), .m_arcache(m_arcache), .m_arprot(m_arprot), .m_arvalid(m_arvalid), .m_arready(m_arready), .m_rdata(m_rdata), .m_rresp(m_rresp), .m_rlast(m_rlast), .m_rvalid(m_rvalid), .m_rready(m_rready), .m_awaddr(m_awaddr), .m_awlen(m_awlen), .m_awsize(m_awsize), .m_awburst(m_awburst), .m_awcache(m_awcache), .m_awprot(m_awprot), .m_awvalid(m_awvalid), .m_awready(m_awready), .m_wdata(m_wdata), .m_wstrb(m_wstrb), .m_wlast(m_wlast), .m_wvalid(m_wvalid), .m_wready(m_wready), .m_bresp(m_bresp), .m_bvalid(m_bvalid), .m_bready(m_bready), .emio_i(emio_i) ); // ---- DDR: a memory model behind the high-performance port -------------------------------------- localparam [48:0] BASE = 49'h0_1000_0000; reg [63:0] mem [0:65535]; reg [48:0] q_addr [0:15]; reg [7:0] q_len [0:15]; reg [4:0] q_head = 0, q_tail = 0; reg [8:0] beat = 0; integer seed = 7; wire [4:0] q_used = q_tail - q_head; always @(posedge clk) begin // address channel: accept when there is room, sometimes late m_arready <= 1'b0; if (m_arvalid && !m_arready && q_used < 5'd8 && ($random(seed) & 3) != 0) begin m_arready <= 1'b1; q_addr[q_tail[3:0]] <= m_araddr; q_len[q_tail[3:0]] <= m_arlen; q_tail <= q_tail + 5'd1; if (m_arsize != 3'd3 || m_arburst != 2'b01) begin $display("DMA: bad size or burst"); $finish; end if ((m_araddr[11:0] + ({4'd0, m_arlen} + 12'd1) * 12'd8) > 13'h1000 && m_araddr[11:0] != 12'd0) begin $display("DMA: a burst crosses a 4 KiB page at %h", m_araddr); $finish; end end // data channel: one beat at a time, with gaps if (m_rvalid && m_rready) begin m_rvalid <= 1'b0; if (m_rlast) begin q_head <= q_head + 5'd1; beat <= 0; end else beat <= beat + 9'd1; end else if (!m_rvalid && q_used != 5'd0 && ($random(seed) % 5) != 0) begin m_rvalid <= 1'b1; m_rdata <= mem[(q_addr[q_head[3:0]] - BASE) / 8 + beat]; m_rlast <= (beat == {1'b0, q_len[q_head[3:0]]}); m_rresp <= 2'b00; end end // ---- the write side of the memory model: one burst at a time, with gaps ------------------------ localparam [48:0] WB_BASE = 49'h0_2000_0000; reg [63:0] wmem [0:4095]; reg [48:0] w_addr = 0; reg w_open = 0; always @(posedge clk) begin m_awready <= 1'b0; m_wready <= 1'b0; if (m_bvalid && m_bready) m_bvalid <= 1'b0; if (m_awvalid && !m_awready && !w_open && ($random(seed) & 1) != 0) begin m_awready <= 1'b1; w_addr <= m_awaddr; w_open <= 1'b1; if (m_awsize != 3'd3 || m_awburst != 2'b01) begin $display("write-back: bad size or burst"); $finish; end if ({1'b0, m_awaddr[11:0]} + ({5'd0, m_awlen} + 13'd1) * 13'd8 > 13'h1000) begin $display("write-back: a burst crosses a 4 KiB page at %h", m_awaddr); $finish; end end if (m_wvalid && !m_wready && w_open && ($random(seed) & 3) != 0) m_wready <= 1'b1; if (m_wvalid && m_wready) begin if (m_wstrb != 8'hFF) begin $display("write-back: bad strobes"); $finish; end wmem[(w_addr - WB_BASE) / 8] <= m_wdata; w_addr <= w_addr + 49'd8; if (m_wlast) begin w_open <= 1'b0; m_bvalid <= 1'b1; m_bresp <= 2'b00; end end end // ---- AXI-Lite master tasks -------------------------------------------------------------------- // The master drives just after a clock edge and samples the slave's outputs at the edge, as // they were before it: a handshake completes on the edge where valid and ready were both high. integer line, t; reg a_ok, w_ok, done_hs; task axi_write(input [15:0] word, input [31:0] d); begin @(posedge clk); #1; awaddr = 32'hA000_0000 | ({16'd0, word} << 2); wdata = d; awvalid = 1; wvalid = 1; t = 0; while (awvalid || wvalid) begin @(posedge clk); a_ok = awready; w_ok = wready; #1; if (a_ok) awvalid = 0; if (w_ok) wvalid = 0; t = t + 1; if (t > 1000) begin $display("TIMEOUT write %h at line %0d", word, line); $finish; end end bready = 1; done_hs = 0; t = 0; while (!done_hs) begin @(posedge clk); done_hs = bvalid; #1; t = t + 1; if (t > 1000) begin $display("TIMEOUT b at line %0d", line); $finish; end end bready = 0; end endtask task axi_read(input [15:0] word, output [31:0] d); begin @(posedge clk); #1; araddr = 32'hA000_0000 | ({16'd0, word} << 2); arvalid = 1; t = 0; while (arvalid) begin @(posedge clk); a_ok = arready; #1; if (a_ok) arvalid = 0; t = t + 1; if (t > 1000) begin $display("TIMEOUT ar %h at line %0d", word, line); $finish; end end rready = 1; done_hs = 0; t = 0; while (!done_hs) begin @(posedge clk); done_hs = rvalid; d = rdata; #1; t = t + 1; if (t > 1000) begin $display("TIMEOUT r at line %0d", line); $finish; end end rready = 0; end endtask integer fd, r, errors, reads, k, n, i, polls, longest; reg [31:0] op, a, d, m, got, word; reg [63:0] ent_a, ent_d; initial begin errors = 0; reads = 0; line = 0; longest = 0; repeat (20) @(posedge clk); rstn = 1; repeat (5) @(posedge clk); if (emio_i[7:0] !== 8'hA5) begin $display("no signature"); $finish; end axi_read(16'h8000, got); if (got !== 32'h4C415849) begin $display("wrapper ID %h", got); $finish; end fd = $fopen("landscape_axi_tb.script", "r"); if (fd == 0) begin $display("no script"); $finish; end r = $fscanf(fd, "%s", op); while (r == 1) begin line = line + 1; if (op == "W") begin r = $fscanf(fd, " %h %h", a, d); axi_write(a[15:0], d); end else if (op == "R") begin r = $fscanf(fd, " %h %h %h", a, d, m); axi_read(a[15:0], got); reads = reads + 1; if ((got & m) !== (d & m)) begin errors = errors + 1; if (errors <= 25) $display("MISMATCH line %0d: R %h got %h want %h", line, a[15:0], got, d); end end else if (op == "I") begin got = 1; polls = 0; while (got[0]) begin axi_read(16'h0002, got); polls = polls + 1; if (polls > 200000) begin $display("TIMEOUT busy at line %0d", line); $finish; end end end else if (op == "D") begin r = $fscanf(fd, " %d", n); for (k = 0; k < n; k = k + 1) begin r = $fscanf(fd, " %h %h", ent_a, ent_d); mem[k] = {16'd0, ent_a[15:0], ent_d[31:0]}; line = line + 1; end axi_write(16'h8002, BASE[31:0]); axi_write(16'h8003, {15'd0, BASE[48:32]}); axi_write(16'h8004, n); axi_write(16'h8005, 1); got = 2; polls = 0; while (got[1]) begin axi_read(16'h8001, got); polls = polls + 1; if (polls > 200000) begin $display("TIMEOUT DMA at line %0d", line); $finish; end end if (got[2]) begin $display("DMA error at line %0d", line); errors = errors + 1; end end else if (op == "Q") begin r = $fscanf(fd, " %d", n); axi_write(16'h8010, n); axi_write(16'h8011, 1); // an engine access while the sequencer holds the engine: answered at once, dropped axi_read(16'h0000, got); if (got !== 32'hDEADBEEF) begin $display("an engine read during a batch returned %h at line %0d", got, line); errors = errors + 1; end axi_read(16'h8008, got); if (got !== 32'd1) begin $display("DROPS %0d after one dropped read, line %0d", got, line); errors = errors + 1; end axi_write(16'h8008, 0); got = 1; polls = 0; while (got[0]) begin axi_read(16'h8001, got); polls = polls + 1; if (polls > 2000000) begin $display("TIMEOUT sequencer at line %0d", line); $finish; end end if (polls > longest) longest = polls; end else if (op == "K") begin // the results of the last batch, written back: n chains, six words each, to check r = $fscanf(fd, " %d", n); axi_read(16'h8025, d); axi_write(16'h8020, WB_BASE[31:0]); axi_write(16'h8021, {15'd0, WB_BASE[48:32]}); axi_write(16'h8022, 0); axi_write(16'h8023, n); axi_write(16'h8024, 1); got = 8; polls = 0; while (got[3]) begin axi_read(16'h8001, got); polls = polls + 1; if (polls > 200000) begin $display("TIMEOUT write-back at line %0d", line); $finish; end end if (got[4]) begin $display("write-back error at line %0d", line); errors = errors + 1; end axi_read(16'h8025, got); if (got !== d + 1) begin $display("write-back did not run at line %0d", line); errors = errors + 1; end for (k = 0; k < n; k = k + 1) begin for (i = 0; i < 6; i = i + 1) begin r = $fscanf(fd, " %h", d); line = line + 1; word = (i % 2) ? wmem[k * 4 + i / 2][63:32] : wmem[k * 4 + i / 2][31:0]; reads = reads + 1; if (word !== d) begin errors = errors + 1; if (errors <= 25) $display("MISMATCH line %0d: written back chain %0d word %0d got %h want %h", line, k, i, word, d); end end end end else begin $display("bad op at line %0d", line); $finish; end r = $fscanf(fd, "%s", op); end $display("TB DONE: %0d lines, %0d checked reads, %0d mismatches, sim time %0t", line, reads, errors, $time); $finish; end endmodule