Connecting DUT and TB using interface without modports

Hi!
For some reasons, I have module that can’t be connected using modport:

module test_int (
    // Synchro signal and reset
    input  logic                ACLK,
    input  logic                ARESETN,
    // Interface S_AXIS_DATA
    input  logic                S_AXIS_DATA_TVALID,
    output logic                S_AXIS_DATA_TREADY,
    input  logic                S_AXIS_DATA_TLAST,
    input  logic [31:0]         S_AXIS_DATA_TDATA,
    input  logic [7:0]          S_AXIS_DATA_TUSER,
    // Interface M_AXIS_DATA
    output logic                M_AXIS_DATA_TVALID,
    input  logic                M_AXIS_DATA_TREADY,
    output logic                M_AXIS_DATA_TLAST,
    output logic [31:0]         M_AXIS_DATA_TDATA,
    output logic [7:0]          M_AXIS_DATA_TUSER
);

I also have a SLAVE and MASTER interfaces:

interface com_tb_int_in #(
    parameter TDATA_WIDTH   = 32,
    parameter TUSER_WIDTH   = 32
)
(
    input logic ACLK,
    input logic ARESETN
);

    logic                   S_AXIS_TVALID = 0;
    logic                   S_AXIS_TREADY;
    logic                   S_AXIS_TLAST = 0;
    logic [TDATA_WIDTH-1:0] S_AXIS_TDATA;
    logic [TUSER_WIDTH-1:0] S_AXIS_TUSER;

    clocking cb @(posedge ACLK);
        input   S_AXIS_TREADY;
        output  S_AXIS_TVALID;
        output  S_AXIS_TLAST;
        output  S_AXIS_TDATA;
        output  S_AXIS_TUSER;
    endclocking

endinterface
interface com_tb_int_out #(
    parameter int TDATA_WIDTH = 32,
    parameter int TUSER_WIDTH = 32
)
(
    input logic ACLK,
    input logic ARESETN
);

    logic                       M_AXIS_TVALID;
    logic                       M_AXIS_TREADY = 0;
    logic                       M_AXIS_TLAST;
    logic [TDATA_WIDTH-1:0]     M_AXIS_TDATA;
    logic [TUSER_WIDTH-1:0]     M_AXIS_TUSER;

    clocking cb @(posedge ACLK);
        input   M_AXIS_TVALID;
        output  M_AXIS_TREADY;
        input   M_AXIS_TLAST;
        input   M_AXIS_TDATA;
        input   M_AXIS_TUSER;
    endclocking
endinterface

Can I connect DUT and TB like this:

module test_int_tb (
    input  logic                ACLK,
    input  logic                ARESETN
);

    // S_AXIS_DATA
    com_tb_int_in #(
        .TDATA_WIDTH                    (32),
        .TUSER_WIDTH                    (8)
    ) com_tb_int_in_data_inst (
        .ACLK                           (ACLK),
        .ARESETN                        (ARESETN)
    );

    // M_AXIS_DATA
    com_tb_int_out #(
        .TDATA_WIDTH                    (32),
        .TUSER_WIDTH                    (8)
    ) com_tb_int_out_inst (
        .ACLK                           (ACLK),
        .ARESETN                        (ARESETN)
    );

    // module test_int instantiation
    test_int test_int_inst (
        // Synchro signals and reset
        .ACLK                           (ACLK),
        .ARESETN                        (ARESETN),
        // Interface S_AXIS_DATA
        .S_AXIS_DATA_TVALID             (com_tb_int_in_data_inst.cb.S_AXIS_TVALID),
        .S_AXIS_DATA_TREADY             (com_tb_int_in_data_inst.cb.S_AXIS_TREADY),
        .S_AXIS_DATA_TLAST              (com_tb_int_in_data_inst.cb.S_AXIS_TLAST),
        .S_AXIS_DATA_TDATA              (com_tb_int_in_data_inst.cb.S_AXIS_TDATA),
        .S_AXIS_DATA_TUSER              (com_tb_int_in_data_inst.cb.S_AXIS_TUSER),
        // Interface M_AXIS_DATA
        .M_AXIS_DATA_TVALID             (com_tb_int_out_inst.cb.M_AXIS_TVALID),
        .M_AXIS_DATA_TLAST              (com_tb_int_out_inst.cb.M_AXIS_TLAST),
        .M_AXIS_DATA_TREADY             (com_tb_int_out_inst.cb.M_AXIS_TREADY),
        .M_AXIS_DATA_TDATA              (com_tb_int_out_inst.cb.M_AXIS_TDATA),
        .M_AXIS_DATA_TUSER              (com_tb_int_out_inst.cb.M_AXIS_TUSER)
    );

endmodule

In reply to Pavel:

Have you tried compiling and simulating? Did you get any errors? Did it work as expected?

Modports are considered mainly a design construct and aren’t typically used for verification.

In reply to cgales:

It does not work as I expect.
I added tasks into interfaces responsible for interaction with DUT.

In master interface there is a task send_data that takes an input buffer and sends it to the DUT:

import com_tb_package::*;
 
interface com_tb_int_in #(
    parameter TDATA_WIDTH   = 32,
    parameter TUSER_WIDTH   = 32
)
(
    input logic ACLK,
    input logic ARESETN
);
 
    logic                   S_AXIS_TVALID = 0;
    logic                   S_AXIS_TREADY;
    logic                   S_AXIS_TLAST = 0;
    logic [TDATA_WIDTH-1:0] S_AXIS_TDATA;
    logic [TUSER_WIDTH-1:0] S_AXIS_TUSER;
 
    clocking cb @(posedge ACLK);
        input   S_AXIS_TREADY;
        output  S_AXIS_TVALID;
        output  S_AXIS_TLAST;
        output  S_AXIS_TDATA;
        output  S_AXIS_TUSER;
    endclocking
 
    task automatic send_data (const ref c_data_buffer #(TDATA_WIDTH, TUSER_WIDTH) data_buffer, input logic random_tvalid = 0);
        int i = 0;
 
        assert (data_buffer.size() > 0)
        else begin
            $error("Data buffer is empty. Exitting task");
            return;
        end
 
        while (i <= data_buffer.size() - 1) begin
 
            if ( (random_tvalid && $urandom_range(1, 0) == 1) || !random_tvalid ) begin
 
                cb.S_AXIS_TVALID    <= 1'b1;
                cb.S_AXIS_TDATA     <= data_buffer.bus[i].tdata;
                cb.S_AXIS_TUSER     <= data_buffer.bus[i].tuser;
                cb.S_AXIS_TLAST     <= data_buffer.bus[i].tlast;
 
                if (cb.S_AXIS_TREADY) begin
                    i++;
                end
 
            end else begin
                cb.S_AXIS_TVALID <= 1'b0;
            end
 
            @ (cb);
 
        end
 
        cb.S_AXIS_TVALID <= 1'b0;
 
    endtask
 
endinterface

Slave interface has task get_data that receiving data from DUT until stop_event:

import com_tb_package::*;
 
interface com_tb_int_out #(
    parameter int TDATA_WIDTH = 32,
    parameter int TUSER_WIDTH = 32
)
(
    input logic ACLK,
    input logic ARESETN
);
 
    logic                       M_AXIS_TVALID;
    logic                       M_AXIS_TREADY = 0;
    logic                       M_AXIS_TLAST;
    logic [TDATA_WIDTH-1:0]     M_AXIS_TDATA;
    logic [TUSER_WIDTH-1:0]     M_AXIS_TUSER;
 
    clocking cb @(posedge ACLK);
        input   M_AXIS_TVALID;
        output  M_AXIS_TREADY;
        input   M_AXIS_TLAST;
        input   M_AXIS_TDATA;
        input   M_AXIS_TUSER;
    endclocking
 
    task automatic get_data (input event stop_e, c_data_buffer #(TDATA_WIDTH,TUSER_WIDTH) data_buffer, logic random_tready = 0);
        event push_backed_e;
 
        if (!random_tready) begin
            cb.M_AXIS_TREADY <= 1'b1;
        end
 
        fork begin
 
            fork
 
                begin
 
                    wait (stop_e.triggered);
 
                    wait (push_backed_e.triggered);
 
                end
 
                forever begin
 
                    @(cb);
 
                    if ( (random_tready && $urandom_range(1,0) == 1) || !random_tready) begin
                        cb.M_AXIS_TREADY <= 1'b1;
 
                        if (cb.M_AXIS_TVALID) begin
                            data_buffer.push_back(cb.M_AXIS_TDATA, cb.M_AXIS_TUSER, cb.M_AXIS_TLAST);
                        end
 
                    end else begin
                        cb.M_AXIS_TREADY <= 1'b0;
                    end
 
                    ->push_backed_e;
 
                end
 
            join_any
 
            disable fork;
 
        end join
 
        cb.M_AXIS_TREADY <= 1'b0;
 
    endtask
 
endinterface

DUT is simply generate S_AXIS_DATA_TREADY signal and M_AXIS_DATA_TVALID with counters on M_AXIS_DATA_TDATA and M_AXIS_DATA_TUSER buses:

module test_int (
    // Synchro signal and reset
    input  logic                ACLK,
    input  logic                ARESETN,
    // Interface S_AXIS_DATA
    input  logic                S_AXIS_DATA_TVALID,
    output logic                S_AXIS_DATA_TREADY,
    input  logic                S_AXIS_DATA_TLAST,
    input  logic [31:0]         S_AXIS_DATA_TDATA,
    input  logic [7:0]          S_AXIS_DATA_TUSER,
    // Interface M_AXIS_DATA
    output logic                M_AXIS_DATA_TVALID,
    input  logic                M_AXIS_DATA_TREADY,
    output logic                M_AXIS_DATA_TLAST,
    output logic [31:0]         M_AXIS_DATA_TDATA,
    output logic [7:0]          M_AXIS_DATA_TUSER
);
 
    // Constants
    localparam logic random_tready = 0;
    localparam logic random_tvalid = 0;
 
    // Delayed reset signal (active low)
    (* keep = "true" *) logic   aresetn_into = 1'b0;
 
    // Work signals
    logic [31:0] data_cnt;
    logic [7:0]  user_cnt;
 
    assign M_AXIS_DATA_TDATA = data_cnt;
    assign M_AXIS_DATA_TUSER = user_cnt;
 
    // Delay input ARESETN signal
    always_ff @(posedge ACLK) begin
        aresetn_into <= ARESETN;
    end
 
    //
    always_ff @(posedge ACLK) begin
        if (aresetn_into == 1'b0) begin
            S_AXIS_DATA_TREADY <= 1'b0;
            M_AXIS_DATA_TVALID <= 1'b0;
            data_cnt <= 0;
            user_cnt <= 1;
            M_AXIS_DATA_TLAST <= 0;
        end else begin
 
            if (random_tready && $urandom_range(1, 0) == 1 || !random_tready) begin
                S_AXIS_DATA_TREADY <= 1'b1;
            end else begin
                S_AXIS_DATA_TREADY <= 1'b0;
            end
 
            if (random_tvalid && $urandom_range(1, 0) == 1 || !random_tvalid) begin
                M_AXIS_DATA_TVALID <= 1'b1;
            end else begin
                M_AXIS_DATA_TVALID <= 1'b0;
            end
 
            if (M_AXIS_DATA_TVALID && M_AXIS_DATA_TREADY) begin
                data_cnt <= data_cnt + 1;
                user_cnt <= user_cnt + 1;
            end
 
 
        end
    end
 
endmodule

TB is

module test_int_tb (
    input  logic                ACLK,
    input  logic                ARESETN
);
 
    import com_tb_package::*;
 
    logic [31:0] data[100];
    logic [7:0]  user[100];
 
    event data_sended_e;
    event stop_receiving_e;
 
    // S_AXIS_DATA
    com_tb_int_in #(
        .TDATA_WIDTH                    (32),
        .TUSER_WIDTH                    (8)
    ) com_tb_int_in_data_inst (
        .ACLK                           (ACLK),
        .ARESETN                        (ARESETN)
    );
 
    // M_AXIS_DATA
    com_tb_int_out #(
        .TDATA_WIDTH                    (32),
        .TUSER_WIDTH                    (8)
    ) com_tb_int_out_inst (
        .ACLK                           (ACLK),
        .ARESETN                        (ARESETN)
    );
 
 
    // module test_int instantiation
    test_int test_int_inst (
        // Synchro signals and reset
        .ACLK                           (ACLK),
        .ARESETN                        (ARESETN),
        // Interface S_AXIS_DATA
        .S_AXIS_DATA_TVALID             (com_tb_int_in_data_inst.cb.S_AXIS_TVALID),
        .S_AXIS_DATA_TREADY             (com_tb_int_in_data_inst.cb.S_AXIS_TREADY),
        .S_AXIS_DATA_TLAST              (com_tb_int_in_data_inst.cb.S_AXIS_TLAST),
        .S_AXIS_DATA_TDATA              (com_tb_int_in_data_inst.cb.S_AXIS_TDATA),
        .S_AXIS_DATA_TUSER              (com_tb_int_in_data_inst.cb.S_AXIS_TUSER),
        // Interface M_AXIS_DATA
        .M_AXIS_DATA_TVALID             (com_tb_int_out_inst.cb.M_AXIS_TVALID),
        .M_AXIS_DATA_TLAST              (com_tb_int_out_inst.cb.M_AXIS_TLAST),
        .M_AXIS_DATA_TREADY             (com_tb_int_out_inst.cb.M_AXIS_TREADY),
        .M_AXIS_DATA_TDATA              (com_tb_int_out_inst.cb.M_AXIS_TDATA),
        .M_AXIS_DATA_TUSER              (com_tb_int_out_inst.cb.M_AXIS_TUSER)
    );
 
    // S_AXIS_DATA
    initial begin
        automatic c_data_buffer #(.TDATA_WIDTH (32), .TUSER_WIDTH (8)) s_data_buffer;
        automatic logic random_valid = 0;
 
        for (int i = 0; i <= $size(data) - 1; i++) begin
            data[i] = i;
        end
 
        for (int i = 0; i <= $size(user) - 1; i++) begin
            user[i] = i+1;
        end
 
        s_data_buffer = new;
 
        for (int i = 0; i <= 12; i++) begin
            if (i == 11) begin
                s_data_buffer.push_back(data[i], user[i], 1);
            end else begin
                s_data_buffer.push_back(data[i], user[i], 0);
            end
        end
 
        com_tb_int_in_data_inst.send_data(s_data_buffer, random_valid);
 
        ->data_sended_e;
 
    end
 
    // M_AXIS_DATA
    initial begin
        automatic c_data_buffer #(.TDATA_WIDTH (32), .TUSER_WIDTH (8)) m_data_buffer;
        automatic logic random_ready = 1;
 
        @ (posedge ARESETN);
 
        // start collecting data
        m_data_buffer = new;
 
        fork
 
            com_tb_int_out_inst.get_data(stop_receiving_e, m_data_buffer, random_ready);
 
            begin
 
                wait (data_sended_e.triggered);
 
                repeat (5) @ (posedge ACLK);
 
                ->stop_receiving_e;
 
            end
 
        join
 
        for (int i = 0; i <= m_data_buffer.size() - 1; i++) begin
 
            assert (m_data_buffer.bus[i].tdata == data[i])
            else begin
                $error("Data error! M_AXIS_DATA_TDATA = %8h, expected %8h, word %4d", m_data_buffer.bus[i].tdata, data[i], i);
            end
 
        end
 
        $stop;
 
 
    end
 
 
endmodule // test_int_tb

Send_data task in master interface is working, but get_data doesn’t: first words of cb.M_AXIS_TDATA and cb.M_AXIS_TUSER buses and cb.M_AXIS_TLAST signal is in X state.

In reply to Pavel:

The issue is with how you connect the interfaces to the DUT.

A clocking block is used inside the interface to ensure that all the signals are sampled properly. However, when you connect the interface to the DUT, you want to connect the top level signals. I didn’t realize you were doing this in your first post.


 // module test_int instantiation
    test_int test_int_inst (
        // Synchro signals and reset
        .ACLK                           (ACLK),
        .ARESETN                        (ARESETN),
        // Interface S_AXIS_DATA
        .S_AXIS_DATA_TVALID             (com_tb_int_in_data_inst.S_AXIS_TVALID),
        .S_AXIS_DATA_TREADY             (com_tb_int_in_data_inst.S_AXIS_TREADY),
        .S_AXIS_DATA_TLAST              (com_tb_int_in_data_inst.S_AXIS_TLAST),
        .S_AXIS_DATA_TDATA              (com_tb_int_in_data_inst.S_AXIS_TDATA),
        .S_AXIS_DATA_TUSER              (com_tb_int_in_data_inst.S_AXIS_TUSER),
        // Interface M_AXIS_DATA
        .M_AXIS_DATA_TVALID             (com_tb_int_out_inst.M_AXIS_TVALID),
        .M_AXIS_DATA_TLAST              (com_tb_int_out_inst.M_AXIS_TLAST),
        .M_AXIS_DATA_TREADY             (com_tb_int_out_inst.M_AXIS_TREADY),
        .M_AXIS_DATA_TDATA              (com_tb_int_out_inst.M_AXIS_TDATA),
        .M_AXIS_DATA_TUSER              (com_tb_int_out_inst.M_AXIS_TUSER)
    );

In reply to cgales:

I connected DUT as you mentioned, but cb.S_AXIS_TREADY signal in com_tb_int_in interface arrived delayed by 1 clock cycle according to S_AXIS_DATA_TREADY signal of DUT and two handshakes occurred on the first word.

In reply to Pavel:

The clocking blocks are functioning as they are designed. When S_AXIS_DATA_TREADY changes, the clocking block will wait until the next clock to sample it, hence cb/S_AXIS_TREADY having a 1 clock delay.

In reply to cgales:

Thank you for the answer!
If I get it right, in the task I need to wait cb.S_AXIS_TREADY to avoid additional handshakes. But this is not what I wanted: master must rise cb.S_AXIS_TVALID as soon as he is ready,
he should not wait slave’s cb.S_AXIS_TREADY to do it. And I don’t understand how I can achieve such behavior.

In reply to Pavel:

It is not very clear on what you are trying to accomplish with this environment. I am particularly confused by the test_int() module.

When an AXI4Stream Master and Slave communicate, their signals are tied directly together so that they can handshake appropriately. Instead, in test_int(), you have the Master and Slave disconnected and it is forcing TREADY and TVALID independent of what the Master and Slave are actually signalling. This will cause improper behavior since the Master may not have TVALID asserted, yet test_int() may assert it. Or similarly, the test_int() may assert TREADY even if the Slave doesn’t have it asserted.

I recommend that you remove modify test_int() so that it simply connects the Master directly to the Slave. Also, you don’t want any ff’s in test_int() since that will introduce additional clock delays.

In reply to cgales:

I apologize for confusing you. You are rigth. Test_int module is just a stub for testing interfaces and I designed it wrong.

New module:

module test_int (
    // Synchro signal and reset
    input  logic                ACLK,
    input  logic                ARESETN,
    // Interface S_AXIS_DATA
    input  logic                S_AXIS_DATA_TVALID,
    output logic                S_AXIS_DATA_TREADY,
    input  logic                S_AXIS_DATA_TLAST,
    input  logic [31:0]         S_AXIS_DATA_TDATA,
    input  logic [7:0]          S_AXIS_DATA_TUSER,
    // Interface M_AXIS_DATA
    output logic                M_AXIS_DATA_TVALID,
    input  logic                M_AXIS_DATA_TREADY,
    output logic                M_AXIS_DATA_TLAST,
    output logic [31:0]         M_AXIS_DATA_TDATA,
    output logic [7:0]          M_AXIS_DATA_TUSER
);

    assign M_AXIS_DATA_TDATA = S_AXIS_DATA_TDATA;
    assign M_AXIS_DATA_TUSER = S_AXIS_DATA_TUSER;
    assign S_AXIS_DATA_TREADY = M_AXIS_DATA_TREADY;
    assign M_AXIS_DATA_TVALID = S_AXIS_DATA_TVALID;
    assign M_AXIS_DATA_TLAST = S_AXIS_DATA_TLAST;

endmodule

Waveform with problem:

In reply to Pavel:

What issue are you having now?

There is one issue with the timing in your send data task that you need to resolve. You need to look at when the Master should consider the transaction complete.

In reply to cgales:

Due to the delay in clocking block, the master understands that transaction completed later than the slave. As a result, the slave samples the same word two times instead of one. I don’t understand how to synchronize them.

In reply to Pavel:

This is because your master implementation in send_data() is incorrect. You need to fix it.

In reply to cgales:

I understand it. I see one way to fix the problem, as I mentioned earlier, in the send_data() task I need to wait cb.S_AXIS_TREADY and then trigger cb.S_AXIS_TVALID, something like this:

while (i <= data_buffer.size() - 1) begin

    while (cb.S_AXIS_TREADY == 0 || $isunknown(cb.S_AXIS_TREADY)) begin
        @ (cb);
    end

    if ( (random_tvalid && $urandom_range(1, 0) == 1) || !random_tvalid ) begin

        cb.S_AXIS_TVALID    <= 1'b1;
        cb.S_AXIS_TDATA     <= data_buffer.bus[i].tdata;
        cb.S_AXIS_TUSER     <= data_buffer.bus[i].tuser;
        cb.S_AXIS_TLAST     <= data_buffer.bus[i].tlast;

        i++;

    end else begin
        cb.S_AXIS_TVALID <= 1'b0;
    end

    @ (cb);

end

while (cb.S_AXIS_TREADY == 0 || $isunknown(cb.S_AXIS_TREADY)) begin
    @ (cb);
end

cb.S_AXIS_TVALID <= 1'b0;

But this is not a right solution. I don’t know how to do it rigth.

In reply to Pavel:


while (i <= data_buffer.size() - 1) begin
  if ( (random_tvalid && $urandom_range(1, 0) == 1) || !random_tvalid ) begin
    cb.S_AXIS_TVALID    <= 1'b1;
    cb.S_AXIS_TDATA     <= data_buffer.bus[i].tdata;
    cb.S_AXIS_TUSER     <= data_buffer.bus[i].tuser;
    cb.S_AXIS_TLAST     <= data_buffer.bus[i].tlast;
    @(cb);
    while (!cb.S_AXIS_TREADY) @(cb);
    i++;
  end else begin
    cb.S_AXIS_TVALID <= 1'b0;
    @(cb);
  end
end

In reply to cgales:

Thank you, your solution is very helpful! But there is a moment: when signal cb.S_AXIS_TREADY rises before cb.S_AXIS_TVALID, the first word is skipped.

Is this some timing issue? I have the feeling that code evaluated before cb event and the expression @(cb); after cb.S_AXIS_TLAST <= data_buffer.bus[i].tlast; row just waited for nearest event but not full clock cycle.

I decided to add @(cb) in the beginning (before while loop) and it start working.

Am I right?

In reply to Pavel:

No. The problem is that you don’t handle ARESETN anywhere in com_tb_int_in. I’m guessing that you use it somewhere else that isn’t shown which is preventing the signals from being driven correctly and is conflicting with the while() loop.

In reply to cgales:

I’m not sure that this is the reason. In testbench I run the task after posedge of ARESETN signal:

    // S_AXIS_DATA
    initial begin
        automatic c_data_buffer #(.TDATA_WIDTH (32), .TUSER_WIDTH (8)) s_data_buffer;
        automatic logic random_valid = 0;

        @ (posedge ARESETN);

        @ (posedge ACLK);

        for (int i = 0; i <= $size(data) - 1; i++) begin
            data[i] = i;
        end

        for (int i = 0; i <= $size(user) - 1; i++) begin
            user[i] = i+1;
        end

        s_data_buffer = new;

        for (int i = 0; i <= $size(user) - 1; i++) begin
            if ( (i > 0 && (i % 10) == 0) || i == ($size(user) - 1) ) begin
                s_data_buffer.push_back(data[i], user[i], 1);
            end else begin
                s_data_buffer.push_back(data[i], user[i], 0);
            end
        end

        com_tb_int_in_data_inst.send_data(s_data_buffer, random_valid);

        ->data_sended_e;

    end

In reply to Pavel:

You need to ensure that both com_tb_int_in and comp_tb_int_out are sensitive to ARESETN as well. This signal should be part of the clocking block to synchronize it with the clock.

Per the AXI4Stream spec, when ARESETN is low, TVALID is required to be low, which doesn’t happen because transactor is not fully compliant.

In reply to cgales:

Thank you very much, I will consider this in the interfaces