//
// Copyright 2020 Ettus Research, A National Instruments Brand
//
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Module: PkgAxiStreamBfm
//
// Description: Package for a bi-directional AXI Stream bus functional model
// (BFM). This consists of the AxiStreamPacket and AxiStreamBfm classes.
// It's based on the AxiStreamIf in lib/axi4s_svPkgAXI4S.sv
//


//-----------------------------------------------------------------------------
// AXI-Stream BFM Package
//-----------------------------------------------------------------------------

package PkgAxiStreamBfm;

  //---------------------------------------------------------------------------
  // Raw packets - packet of just bytes
  //---------------------------------------------------------------------------

  // Ethernet in particular defies normal word boundaries, so underneath treating
  // it as the most fundamental quantity - Bytes.
  typedef byte raw_pkt_t[$]; // Byte Queue

  // Push a packet with random data onto to the AXI Stream bus
  // Args:
  // - num_bytes: number of random bytes to add to the raw packet.
  // - pkt: packet with rand data
  task automatic get_rand_raw_pkt (
    input int num_bytes,
    output raw_pkt_t pkt);
    begin
      repeat(num_bytes) begin
        pkt.push_back($urandom);
      end
    end
  endtask

  // Push a packet with a ramp on to the AXI Stream bus
  // Args:
  // - num_samps: Packet size in bytes *8
  // - ramp_start: Start value for the ramp
  // - ramp_inc: Increment per clock cycle
  task automatic get_ramp_raw_pkt (
    input int num_samps,
    input logic [63:0] ramp_start,
    input logic [63:0] ramp_inc,
    input int SWIDTH=64,
    output raw_pkt_t pkt);
    begin
      logic[63:0]       word;
      automatic integer counter = 0;
      repeat(num_samps) begin
        word = ramp_start+(counter*ramp_inc);
        for (int i=0; i < SWIDTH ; i+=8) begin
          pkt.push_back(word[i +: 8]);
        end
        counter = counter + 1;
      end
    end
  endtask

  // Comparison Functions
  function automatic bit raw_pkt_compare(input raw_pkt_t a, input raw_pkt_t b);

    bit queue_match;
    queue_match = 1;
    // check each element of the queue and clear queue_match if they don't match.
    // workaround for Vivado bug - could be a==b
    foreach(a[i]) queue_match = queue_match && a[i] == b[i];
    return ((a.size() == b.size()) && queue_match);

  endfunction

  //---------------------------------------------------------------------------
  // AXI Stream Packet Class
  //---------------------------------------------------------------------------

  class AxiStreamPacket #(DATA_WIDTH = 64, USER_WIDTH = 1);

    //------------------
    // Type Definitions
    //------------------

    // Round up width of TKEEP when TDATA is not a multiple of 8
    localparam int KEEP_WIDTH = (DATA_WIDTH+7)/8;

    typedef logic [DATA_WIDTH-1:0]   data_t;  // Single bus TDATA word
    typedef logic [KEEP_WIDTH-1:0]   keep_t;  // Single TKEEP word
    typedef logic [USER_WIDTH-1:0]   user_t;  // Single TUSER word

    typedef AxiStreamPacket #(DATA_WIDTH, USER_WIDTH) AxisPacket_t;

    bit verbose=0;
    //------------
    // Properties
    //------------

    data_t data[$];
    user_t user[$];
    keep_t keep[$];


    //---------
    // Methods
    //---------

    // Return a handle to a copy of this transaction
    function AxisPacket_t copy();
      AxisPacket_t temp;
      temp = new();
      temp.data = this.data;
      temp.user = this.user;
      temp.keep = this.keep;
      return temp;
    endfunction


    // Delete the contents of the current packet
    function void empty();
      data = {};
      user = {};
      keep = {};
    endfunction;

    // Delete a word from the current packet
    function void delete(int i);
      data.delete(i);
      user.delete(i);
      keep.delete(i);
    endfunction;

    // Return true if this packet equals that of the argument
    virtual function bit equal(AxisPacket_t packet);
      // These variables are needed to workaround Vivado queue support issues
      data_t data_a, data_b;
      user_t user_a, user_b;
      keep_t keep_a, keep_b;

      if (data.size() != packet.data.size()) return 0;
      foreach (data[i]) begin
        data_a = data[i];
        data_b = packet.data[i];
        if (data_a !== data_b) begin
          if (verbose) $display("AxisPacket data mismatch a[%2d]=%X b[%2d]=%X",i,data_a,i,data_b);
          return 0;
        end
      end

      if (user.size() != packet.user.size()) return 0;
      foreach (data[i]) begin
        user_a = user[i];
        user_b = packet.user[i];
        if (user_a !== user_b) begin
          if (verbose) $display("AxisPacket user mismatch a[%2d]=%X b[%2d]=%X",i,user_a,i,user_b);
          return 0;
        end
      end

      if (keep.size() != packet.keep.size()) return 0;
      foreach (keep[i]) begin
        keep_a = keep[i];
        keep_b = packet.keep[i];
        if (keep_a !== keep_b) begin
          if (verbose) $display("AxisPacket keep mismatch a[%2d]=%X b[%2d]=%X",i,user_a,i,user_b);
          return 0;
        end
      end

      return 1;
    endfunction : equal


    // Format the contents of the packet into a string
    function string sprint();
      string str = "";
      string data_str = "";
      if (data.size() == user.size() && data.size() == keep.size()) begin
        str = { str, "data, user, keep:\n" };
        foreach (data[i]) begin
          data_str = "";
          if (DATA_WIDTH > 64) begin
            for (int b=0; b < DATA_WIDTH; b +=64) begin
              data_str = { data_str, $sformatf("%3d: %X",b,data[i][b+:64])};
              if (b+64 < DATA_WIDTH) begin
                data_str = { data_str, $sformatf("\n       ")};
              end
            end
          end else begin
            data_str = { data_str, $sformatf("%X",data[i]) };
          end
          str = { str, $sformatf("%5d> %s %X %X \n", i, data_str, user[i], keep[i]) };
        end
      end else begin
        str = { str, "data:\n" };
        foreach (data[i]) begin
          str = { str, $sformatf("%5d> %X\n", i, data[i]) };
        end
        str = { str, "user:\n" };
        foreach (user[i]) begin
          str = { str, $sformatf("%5d> %X\n", i, user[i]) };
        end
        str = { str, "keep:\n" };
        foreach (keep[i]) begin
          str = { str, $sformatf("%5d> %X\n", i, keep[i]) };
        end
      end
      return str;
    endfunction : sprint


    // Print the contents of the packet
    function void print();
      $display(sprint());
    endfunction : print

    // Add an array of bytes (little endian)
    function void push_bytes(raw_pkt_t raw, input user_t user = '0);
      data_t word;
      keep_t my_keep;
      while (raw.size() > 0) begin
         // fill tkeep
         // SIZE = TKEEP / 0 = 0000, 1 = 0001, 2 = 0011, etc
         my_keep = '1;
         if (raw.size <= DATA_WIDTH/8) begin
            foreach (my_keep[i]) my_keep[i] = i < (raw.size);
         end
         // fill the word with raw data from bottom up
         word = '0;
         for (int i = 0; i < DATA_WIDTH/8 ; i++) begin
            if (my_keep[i]) word[i*8 +: 8] = raw.pop_front();
         end
         this.data.push_back(word);
         this.keep.push_back(my_keep);
         this.user.push_back(user);
      end
    endfunction

    // Dump data contents as an array of bytes. (little endian)
    function raw_pkt_t dump_bytes();
      data_t    word;
      keep_t    my_keep;
      raw_pkt_t raw;
      assert (data.size == keep.size) else
         $fatal(1, "data and keep have different sizes!");
      foreach (data[i]) begin
         my_keep = this.keep[i];
         word    = this.data[i];
         for (int j = 0; j < DATA_WIDTH/8 ; j++) begin
            if (my_keep[j] !== 1'b0) raw.push_back(word[j*8 +: 8]);
         end;
      end
      return raw;
    endfunction

  endclass : AxiStreamPacket;



  //---------------------------------------------------------------------------
  // AXI Stream BFM Class
  //---------------------------------------------------------------------------

  // Reset behavior: determines how the BFM slave handles packets in progress when
  // a reset occurs.
  typedef enum  {
    DISCARD_PACKET,  // DISCARD_PACKET  - discard any in-progress output packets on reset
    CONTINUE_PACKET, // CONTINUE_PACKET - continue sending/receiving packet in progress during reset
    TRUNCATE_PACKET  // TRUNCATE_PACKET - ignore DUT outputs for the duration of the reset
  } reset_behavior_t;

  class AxiStreamBfm #(
    int DATA_WIDTH = 64,
    int USER_WIDTH = 1,
    int MAX_PACKET_BYTES = 0,
    bit TDATA = 1,
    bit TUSER = 1,
    bit TKEEP = 1,
    bit TLAST = 1,
    reset_behavior_t RESET_BEHAVIOR = DISCARD_PACKET
  );

    //------------------
    // Type Definitions
    //------------------

    typedef AxiStreamPacket #(DATA_WIDTH, USER_WIDTH) AxisPacket_t;
    typedef AxisPacket_t::data_t data_t;
    typedef AxisPacket_t::user_t user_t;
    typedef AxisPacket_t::keep_t keep_t;


    //------------
    // Properties
    //------------

    localparam int KEEP_WIDTH = AxisPacket_t::KEEP_WIDTH;

    // Default stall probability, as a percentage (0-100).
    local const int DEF_STALL_PROB = 38;

    // Default values to use for idle bus cycles
    local const AxisPacket_t::data_t IDLE_DATA = {DATA_WIDTH{1'bX}};
    local const AxisPacket_t::user_t IDLE_USER = {(USER_WIDTH > 1 ? USER_WIDTH : 1){1'bX}};
    local const AxisPacket_t::keep_t IDLE_KEEP = {KEEP_WIDTH{1'bX}};

    // Virtual interfaces for master and slave connections to DUT
    local virtual AxiStreamIf #(DATA_WIDTH,USER_WIDTH,MAX_PACKET_BYTES,
                            TDATA,TUSER,TKEEP,TLAST).master master;
    local virtual AxiStreamIf #(DATA_WIDTH,USER_WIDTH,MAX_PACKET_BYTES,
                            TDATA,TUSER,TKEEP,TLAST).slave  slave;
    // NOTE: We should not need these flags if Vivado would be OK with null check
    //       without throwing unnecessary null-ptr deref exceptions.
    local bit master_en;
    local bit slave_en;

    bit slave_tready_init = 0;
    // Queues to store the bus transactions
    mailbox #(AxisPacket_t) tx_packets;
    mailbox #(AxisPacket_t) rx_packets;

    // Properties for the stall behavior of the BFM
    protected int master_stall_prob = DEF_STALL_PROB;
    protected int slave_stall_prob  = DEF_STALL_PROB;

    // Number of clocks between packets
    int inter_packet_gap = 0;

    //---------
    // Methods
    //---------

    // Returns 1 if the packets have the same contents, otherwise returns 0.
    function bit packets_equal(AxisPacket_t a, AxisPacket_t b);
      return a.equal(b);
    endfunction : packets_equal


    // Class constructor. This must be given an interface for the master
    // connection and an interface for the slave connection.
    function new(
      virtual AxiStreamIf #(DATA_WIDTH,USER_WIDTH,MAX_PACKET_BYTES,
                            TDATA,TUSER,TKEEP,TLAST).master master,
      virtual AxiStreamIf #(DATA_WIDTH,USER_WIDTH,MAX_PACKET_BYTES,
                            TDATA,TUSER,TKEEP,TLAST).slave  slave
    );
      this.master_en = (master != null);
      this.slave_en = (slave != null);
      this.master = master;
      this.slave  = slave;
      tx_packets = new;
      rx_packets = new;
    endfunction : new


    // Queue the provided packet for transmission
    task put(AxisPacket_t packet);
      assert (master_en) else $fatal(1, "Cannot use TX operations for a null master");
      tx_packets.put(packet);
    endtask : put


    // Attempt to queue the provided packet for transmission. Return 1 if
    // successful, return 0 if the queue is full.
    function bit try_put(AxisPacket_t packet);
      assert (master_en) else $fatal(1, "Cannot use TX operations for a null master");
      return tx_packets.try_put(packet);
    endfunction : try_put


    // Get the next packet when it becomes available (waits if necessary)
    task get(output AxisPacket_t packet);
      assert (slave_en) else $fatal(1, "Cannot use RX operations for a null slave");
      rx_packets.get(packet);
    endtask : get


    // Get the next packet if there's one available and return 1. Return 0 if
    // there's no packet available.
    function bit try_get(output AxisPacket_t packet);
      assert (slave_en) else $fatal(1, "Cannot use RX operations for a null slave");
      return rx_packets.try_get(packet);
    endfunction : try_get


    // Get the next packet when it becomes available (wait if necessary), but
    // don't remove it from the receive queue.
    task peek(output AxisPacket_t packet);
      assert (slave_en) else $fatal(1, "Cannot use RX operations for a null slave");
      rx_packets.peek(packet);
    endtask : peek


    // Get the next packet if there's one available and return 1, but don't
    // remove it from the receive queue. Return 0 if there's no packet
    // available.
    function bit try_peek(output AxisPacket_t packet);
      assert (slave_en) else $fatal(1, "Cannot use RX operations for a null slave");
      return rx_packets.try_peek(packet);
    endfunction : try_peek


    // Return the number of packets available in the receive queue
    function int num_received();
      assert (slave_en) else $fatal(1, "Cannot use RX operations for a null slave");
      return rx_packets.num();
    endfunction


    // Wait until num packets have started transmission (i.e., until num
    // packets have been dequeued). Set num = -1 to wait until all currently
    // queued packets have started transmission.
    task wait_send(int num = -1);
      int end_num;
      assert (master_en) else $fatal(1, "Cannot use TX operations for a null master");

      if (num == -1) end_num = 0;
      else begin
        end_num = tx_packets.num() - num;
        assert(end_num >= 0) else begin
          $fatal(1, "Not enough packets queued to wait for %0d packets", num);
        end
      end
      while(tx_packets.num() > end_num) @(posedge master.clk);
    endtask : wait_send


    // Wait until num packets have completed transmission. Set num = -1 to wait
    // for all currently queued packets to complete transmission.
    task wait_complete(int num = -1);
      int end_num;
      assert (master_en) else $fatal(1, "Cannot use TX operations for a null master");

      if (num == -1) num = tx_packets.num();
      else begin
        assert(num <= tx_packets.num()) else begin
          $fatal(1, "Not enough packets queued to wait for %0d packets", num);
        end
      end

      repeat (num) begin
        @(posedge master.tlast);  // Wait for last word
        do begin                  // Wait until the last word is accepted
          @(posedge master.clk);
        end while(master.tready != 1);
      end
    endtask : wait_complete


    // Set the probability (as a percentage, 0 to 100) of the master interface
    // stalling due to lack of data to send.
    function void set_master_stall_prob(int stall_probability = DEF_STALL_PROB);
      assert(stall_probability >= 0 && stall_probability <= 100) else begin
        $fatal(1, "Invalid master stall_probability value");
      end
      master_stall_prob = stall_probability;
    endfunction


    // Set the probability (as a percentage, 0 to 100) of the slave interface
    // stalling due to lack of buffer space.
    function void set_slave_stall_prob(int stall_probability = DEF_STALL_PROB);
      assert(stall_probability >= 0 && stall_probability <= 100) else begin
        $fatal(1, "Invalid slave stall_probability value");
      end
      slave_stall_prob = stall_probability;
    endfunction


    // Get the probability (as a percentage, 0 to 100) of the master interface
    // stalling due to lack of data to send.
    function int get_master_stall_prob();
      return master_stall_prob;
    endfunction


    // Get the probability (as a percentage, 0 to 100) of the slave interface
    // stalling due to lack of buffer space.
    function int get_slave_stall_prob();
      return slave_stall_prob;
    endfunction


    // Determine if the slave interface is doing a transfer this clock
    function logic slave_idle();
      return !slave.tvalid;
    endfunction


    // Create separate processes for driving the master and slave interfaces
    task run();
      fork
        if (master_en) master_body();
        if (slave_en)  slave_body();
      join_none
    endtask


    //----------------
    // Master Process
    //----------------

    local task master_body();
      AxisPacket_t packet;

      master.tvalid <= 0;
      master.tdata  <= IDLE_DATA;
      master.tuser  <= IDLE_USER;
      master.tkeep  <= IDLE_KEEP;
      master.tlast  <= 0;

      forever begin
        repeat(inter_packet_gap) begin
          @(posedge master.clk);
          if (master.rst) continue;
        end

        if (tx_packets.try_get(packet)) begin
          foreach (packet.data[i]) begin
            // Randomly deassert tvalid for next word and stall
            if ($urandom_range(99) < master_stall_prob) begin
              master.tvalid <= 0;
              master.tdata  <= IDLE_DATA;
              master.tuser  <= IDLE_USER;
              master.tkeep  <= IDLE_KEEP;
              master.tlast  <= 0;
              do begin
                @(posedge master.clk);
                if (master.rst) break;
              end while ($urandom_range(99) < master_stall_prob);
              if (master.rst) break;
            end

            // Send the next word
            master.tvalid <= 1;
            master.tdata  <= packet.data[i];
            master.tuser  <= packet.user[i];
            master.tkeep  <= packet.keep[i];
            if (i == packet.data.size()-1) master.tlast <= 1;

            do begin
              @(posedge master.clk);
              if (master.rst) break;
            end while (!master.tready);
          end
          master.tvalid <= 0;
          master.tdata  <= IDLE_DATA;
          master.tuser  <= IDLE_USER;
          master.tkeep  <= IDLE_KEEP;
          master.tlast  <= 0;
        end else begin
          @(posedge master.clk);
          if (master.rst) continue;
        end
      end
    endtask : master_body


    //---------------
    // Slave Process
    //---------------

    local task slave_body();
      AxisPacket_t packet = new();

      slave.tready <= slave_tready_init;

      forever begin
        @(posedge slave.clk);
        if (slave.rst) begin
          case (RESET_BEHAVIOR)
            DISCARD_PACKET: begin
              packet.data = {};
              packet.user = {};
              packet.keep = {};
              continue;
            end
            CONTINUE_PACKET: begin
              // Do nothing, continue receiving the packet
            end
            TRUNCATE_PACKET: begin
              continue;
            end
            default: begin
              $fatal(1, "Unrecognized RESET_BEHAVIOR value: %s", RESET_BEHAVIOR.name());
            end
          endcase
        end

        if (slave.tvalid) begin
          if (slave.tready) begin
            packet.data.push_back(slave.tdata);
            packet.user.push_back(slave.tuser);
            packet.keep.push_back(slave.tkeep);
            if (slave.tlast) begin
              rx_packets.put(packet.copy());
              packet.data = {};
              packet.user = {};
              packet.keep = {};
            end
          end
          slave.tready <= $urandom_range(99) < slave_stall_prob ? 0 : 1;
        end
      end
    endtask : slave_body

  endclass : AxiStreamBfm


endpackage : PkgAxiStreamBfm
