rfnoc: lib: introduce ctrl-port interface and BFM
Original-commit: 886cf97ad5d494b20c6fff0a53113f6096d458c7
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright 2018 Ettus Research, A National Instruments Company
|
# Copyright 2024 Ettus Research, a National Instruments Brand
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: LGPL-3.0-or-later
|
# SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
#
|
#
|
||||||
@@ -15,8 +15,9 @@ ctrlport.vh \
|
|||||||
))
|
))
|
||||||
|
|
||||||
RFNOC_CORE_SRCS = $(abspath $(addprefix $(BASE_DIR)/../lib/rfnoc/core/, \
|
RFNOC_CORE_SRCS = $(abspath $(addprefix $(BASE_DIR)/../lib/rfnoc/core/, \
|
||||||
rfnoc_chdr_utils_pkg.sv \
|
|
||||||
ctrlport_pkg.sv \
|
ctrlport_pkg.sv \
|
||||||
|
ctrlport_if.sv \
|
||||||
|
rfnoc_chdr_utils_pkg.sv \
|
||||||
axis_ctrl_endpoint.v \
|
axis_ctrl_endpoint.v \
|
||||||
axis_ctrl_master.v \
|
axis_ctrl_master.v \
|
||||||
axis_ctrl_slave.v \
|
axis_ctrl_slave.v \
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2024 Ettus Research, a National Instruments Brand
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
//
|
||||||
|
// Interface: ctrlport_if
|
||||||
|
//
|
||||||
|
// Description:
|
||||||
|
//
|
||||||
|
// Defines a SystemVerilog interface for the control-port bus.
|
||||||
|
//
|
||||||
|
|
||||||
|
interface ctrlport_if (
|
||||||
|
input logic clk,
|
||||||
|
input logic rst = 1'b0
|
||||||
|
);
|
||||||
|
|
||||||
|
import ctrlport_pkg::*;
|
||||||
|
|
||||||
|
// Consisting of a request and a response moving in different directions.
|
||||||
|
ctrlport_request_t req;
|
||||||
|
ctrlport_response_t resp;
|
||||||
|
|
||||||
|
// Master driving the request and consuming the response.
|
||||||
|
modport master (
|
||||||
|
input clk,
|
||||||
|
input rst,
|
||||||
|
|
||||||
|
output req,
|
||||||
|
input resp
|
||||||
|
);
|
||||||
|
|
||||||
|
// Slave consuming the request and driving the response.
|
||||||
|
modport slave (
|
||||||
|
input clk,
|
||||||
|
input rst,
|
||||||
|
|
||||||
|
input req,
|
||||||
|
output resp
|
||||||
|
);
|
||||||
|
|
||||||
|
endinterface : ctrlport_if
|
||||||
@@ -14,4 +14,34 @@ package ctrlport_pkg;
|
|||||||
|
|
||||||
`include "ctrlport.vh"
|
`include "ctrlport.vh"
|
||||||
|
|
||||||
|
// typedef for status
|
||||||
|
typedef enum logic [1:0] {
|
||||||
|
STS_OKAY = CTRL_STS_OKAY,
|
||||||
|
STS_CMDERR = CTRL_STS_CMDERR,
|
||||||
|
STS_TSERR = CTRL_STS_TSERR,
|
||||||
|
STS_WARNING = CTRL_STS_WARNING } ctrlport_status_t;
|
||||||
|
|
||||||
|
// Defining signals as defined in Table 18 of RFNoC Specification v.1.0.1.
|
||||||
|
// request related signals
|
||||||
|
typedef struct packed {
|
||||||
|
logic wr;
|
||||||
|
logic rd;
|
||||||
|
logic [ CTRLPORT_ADDR_W-1:0] addr;
|
||||||
|
logic [ CTRLPORT_PORTID_W-1:0] port_id;
|
||||||
|
logic [CTRLPORT_REM_EPID_W-1:0] remote_epid;
|
||||||
|
logic [ CTRLPORT_PORTID_W-1:0] remote_portid;
|
||||||
|
logic [ CTRLPORT_DATA_W-1:0] data;
|
||||||
|
logic [ CTRLPORT_BYTE_EN_W-1:0] byte_en;
|
||||||
|
logic has_time;
|
||||||
|
// signal time - renamed to timestamp to avoid conflict with SystemVerilog keyword time
|
||||||
|
logic [ CTRLPORT_TIME_W-1:0] timestamp;
|
||||||
|
} ctrlport_request_t;
|
||||||
|
|
||||||
|
// response related signals
|
||||||
|
typedef struct packed {
|
||||||
|
logic ack;
|
||||||
|
ctrlport_status_t status;
|
||||||
|
logic [CTRLPORT_DATA_W-1:0] data;
|
||||||
|
} ctrlport_response_t;
|
||||||
|
|
||||||
endpackage : ctrlport_pkg
|
endpackage : ctrlport_pkg
|
||||||
|
|||||||
@@ -31,4 +31,10 @@ noc_shell_generic_ctrlport_pyld_chdr.v \
|
|||||||
timekeeper.v \
|
timekeeper.v \
|
||||||
ctrlport_terminator.v \
|
ctrlport_terminator.v \
|
||||||
chdr_strip_header.sv \
|
chdr_strip_header.sv \
|
||||||
|
ctrlport_if_clk_cross.sv \
|
||||||
|
ctrlport_if_combiner.sv \
|
||||||
|
ctrlport_if_decoder.sv \
|
||||||
|
ctrlport_if_splitter.sv \
|
||||||
|
ctrlport_if_terminator.sv \
|
||||||
|
ctrlport_if_window.sv \
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2025 Ettus Research, a National Instruments Brand
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
//
|
||||||
|
// Module: ctrlport_if_clk_cross
|
||||||
|
//
|
||||||
|
// Description:
|
||||||
|
//
|
||||||
|
// Crosses control-port interface between different clock domains.
|
||||||
|
//
|
||||||
|
|
||||||
|
|
||||||
|
module ctrlport_if_clk_cross
|
||||||
|
(
|
||||||
|
ctrlport_if.slave s_ctrlport,
|
||||||
|
ctrlport_if.master m_ctrlport
|
||||||
|
|
||||||
|
);
|
||||||
|
import ctrlport_pkg::*;
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
// Slave to Master Clock Crossing (Request)
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
ctrlport_request_t m_req_hs;
|
||||||
|
logic m_req_hs_valid;
|
||||||
|
|
||||||
|
// Busy flag can be ignored as the response handshake takes at least the same
|
||||||
|
// amount of cycles to transfer the response as this handshake instance needs
|
||||||
|
// to release the busy flag as they are configured with the same amount of
|
||||||
|
// synchronization stages. Furthermore the ctrlport protocol just allows for
|
||||||
|
// one transaction to be active at the same time. A request can only be issued
|
||||||
|
// once the response is provided.
|
||||||
|
handshake #(
|
||||||
|
.WIDTH($bits(ctrlport_request_t))
|
||||||
|
) req_handshake_inst (
|
||||||
|
.clk_a(s_ctrlport.clk),
|
||||||
|
.rst_a(s_ctrlport.rst),
|
||||||
|
.valid_a((s_ctrlport.req.rd | s_ctrlport.req.wr) & ~s_ctrlport.rst),
|
||||||
|
.data_a(s_ctrlport.req),
|
||||||
|
.busy_a(),
|
||||||
|
.clk_b(m_ctrlport.clk),
|
||||||
|
.valid_b(m_req_hs_valid),
|
||||||
|
.data_b(m_req_hs)
|
||||||
|
);
|
||||||
|
|
||||||
|
always_comb begin
|
||||||
|
m_ctrlport.req = m_req_hs;
|
||||||
|
// mask read and write flags
|
||||||
|
m_ctrlport.req.wr = m_req_hs.wr & m_req_hs_valid & ~m_ctrlport.rst;
|
||||||
|
m_ctrlport.req.rd = m_req_hs.rd & m_req_hs_valid & ~m_ctrlport.rst;
|
||||||
|
end
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
// Master to Slave Clock Crossing (Response)
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
ctrlport_response_t s_resp_hs;
|
||||||
|
logic s_resp_hs_valid;
|
||||||
|
|
||||||
|
// Busy flag can be ignored as the request handshake takes at least the same
|
||||||
|
// amount of cycles to transfer the request as this handshake instance needs
|
||||||
|
// to release the busy flag as they are configured with the same amount of
|
||||||
|
// synchronization stages. Furthermore the ctrlport protocol just allows for
|
||||||
|
// one transaction to be active at the same time. A response can only be
|
||||||
|
// issued once the request is available.
|
||||||
|
handshake #(
|
||||||
|
.WIDTH($bits(ctrlport_response_t))
|
||||||
|
) resp_handshake_inst (
|
||||||
|
.clk_a(m_ctrlport.clk),
|
||||||
|
.rst_a(m_ctrlport.rst),
|
||||||
|
.valid_a(m_ctrlport.resp.ack & ~m_ctrlport.rst),
|
||||||
|
.data_a(m_ctrlport.resp),
|
||||||
|
.busy_a(),
|
||||||
|
.clk_b(s_ctrlport.clk),
|
||||||
|
.valid_b(s_resp_hs_valid),
|
||||||
|
.data_b(s_resp_hs)
|
||||||
|
);
|
||||||
|
|
||||||
|
always_comb begin
|
||||||
|
s_ctrlport.resp = s_resp_hs;
|
||||||
|
s_ctrlport.resp.ack = s_resp_hs.ack & s_resp_hs_valid & ~s_ctrlport.rst;
|
||||||
|
end
|
||||||
|
|
||||||
|
endmodule
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2025 Ettus Research, a National Instruments Brand
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
//
|
||||||
|
// Module: ctrlport_if_combiner
|
||||||
|
//
|
||||||
|
// Description:
|
||||||
|
//
|
||||||
|
// This block is an arbiter that merges control-port interfaces. This block is
|
||||||
|
// used when you have multiple control-port masters that need to access a
|
||||||
|
// single slave. For example, a NoC block with multiple submodules that each
|
||||||
|
// need to read and/or write registers outside of themselves.
|
||||||
|
//
|
||||||
|
// This module combines the control-port requests from multiple masters into a
|
||||||
|
// single request for one slave. Simultaneous requests are handled in the order
|
||||||
|
// specified by PRIORITY. The responding ACK is routed back to the requester.
|
||||||
|
//
|
||||||
|
// The module has been designed so that the latency through it is always the
|
||||||
|
// same when PRIORITY=1 and there is no contention, so that it can be used in
|
||||||
|
// applications where deterministic behavior is desired.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
//
|
||||||
|
// NUM_MASTERS : The number of control-port masters to connect to a single
|
||||||
|
// control-port slave.
|
||||||
|
// PRIORITY : Use PRIORITY = 0 for round robin arbitration, PRIORITY = 1
|
||||||
|
// for priority arbitration (lowest number port serviced first).
|
||||||
|
// TIMEOUT : Number of cycles to wait for a response before aborting the wait for an ack.
|
||||||
|
// TIMEOUT = 0 will not add a timeout.
|
||||||
|
//
|
||||||
|
|
||||||
|
|
||||||
|
module ctrlport_if_combiner #(
|
||||||
|
int NUM_MASTERS = 2,
|
||||||
|
bit PRIORITY = 0,
|
||||||
|
int TIMEOUT = 0
|
||||||
|
) (
|
||||||
|
// Slave Interfaces
|
||||||
|
ctrlport_if.slave s_ctrlport [NUM_MASTERS-1:0],
|
||||||
|
// Master Interface
|
||||||
|
ctrlport_if.master m_ctrlport
|
||||||
|
);
|
||||||
|
|
||||||
|
import ctrlport_pkg::*;
|
||||||
|
|
||||||
|
logic [$clog2(NUM_MASTERS)-1:0] slave_sel = '0; // Tracks which slave port is
|
||||||
|
// currently being serviced.
|
||||||
|
logic req_load_output = '0;
|
||||||
|
logic timeout_occurred = '0;
|
||||||
|
|
||||||
|
// Helper function to convert one hot vector to binary index
|
||||||
|
// (LSB = index 0)
|
||||||
|
function integer one_hot_to_binary(input [NUM_MASTERS-1:0] one_hot_vec);
|
||||||
|
integer i, total;
|
||||||
|
begin
|
||||||
|
total = 0;
|
||||||
|
for (i = 0; i < NUM_MASTERS; i++) begin
|
||||||
|
if (one_hot_vec[i]) begin
|
||||||
|
total = total + i;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
one_hot_to_binary = total;
|
||||||
|
end
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
// Input Registers
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// Latch each request until it can be serviced. Only one request per slave
|
||||||
|
// can be in progress at a time.
|
||||||
|
//
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
ctrlport_request_t req_buffer [NUM_MASTERS-1:0];
|
||||||
|
logic [NUM_MASTERS-1:0] req_valid;
|
||||||
|
|
||||||
|
for (genvar i = 0; i < NUM_MASTERS; i++) begin : gen_input_regs
|
||||||
|
always_ff @(posedge m_ctrlport.clk) begin
|
||||||
|
if (m_ctrlport.rst) begin
|
||||||
|
req_valid[i] <= '0;
|
||||||
|
end else begin
|
||||||
|
if (s_ctrlport[i].req.wr | s_ctrlport[i].req.rd) begin
|
||||||
|
// Mark this slave's request valid and save the request information
|
||||||
|
req_valid[i] <= '1;
|
||||||
|
end
|
||||||
|
|
||||||
|
// Clear the active request when it gets output
|
||||||
|
if (req_load_output && (i == slave_sel)) begin
|
||||||
|
req_valid[i] <= '0;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
// Save buffer without reset
|
||||||
|
if (s_ctrlport[i].req.wr | s_ctrlport[i].req.rd) begin
|
||||||
|
req_buffer[i] <= s_ctrlport[i].req;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
// Arbitration State Machine
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// This state machine tracks which slave port is being serviced and which to
|
||||||
|
// service next. This is done using a counter that simply checks each port in
|
||||||
|
// sequential order and then stops when it finds one that has a valid request.
|
||||||
|
//
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
logic req_active = '0; // Indicates if there's a request being serviced
|
||||||
|
logic [NUM_MASTERS-1:0] next_slave_one_hot; // one hot for next active request
|
||||||
|
// (used for PRIORITY = 1)
|
||||||
|
|
||||||
|
for (genvar i = 0; i < NUM_MASTERS; i++) begin : gen_next_slave_one_hot
|
||||||
|
if (i == 0) begin
|
||||||
|
assign next_slave_one_hot[i] = req_valid[i];
|
||||||
|
end else begin
|
||||||
|
assign next_slave_one_hot[i] = req_valid[i] & ~next_slave_one_hot[i-1];
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
always_ff @(posedge m_ctrlport.clk) begin
|
||||||
|
if (m_ctrlport.rst) begin
|
||||||
|
slave_sel <= '0;
|
||||||
|
req_active <= '0;
|
||||||
|
req_load_output <= '0;
|
||||||
|
end else begin
|
||||||
|
req_load_output <= '0;
|
||||||
|
|
||||||
|
if (req_active) begin
|
||||||
|
// Wait until we get the response before we allow another request
|
||||||
|
if (m_ctrlport.resp.ack || timeout_occurred) begin
|
||||||
|
req_active <= '0;
|
||||||
|
|
||||||
|
// Go to next slave immediately
|
||||||
|
if(PRIORITY == 1)
|
||||||
|
slave_sel <= one_hot_to_binary(next_slave_one_hot);
|
||||||
|
// Round robin - Go to the next slave so we don't service the same
|
||||||
|
// slave again
|
||||||
|
else if(slave_sel == NUM_MASTERS-1)
|
||||||
|
slave_sel <= '0;
|
||||||
|
else
|
||||||
|
slave_sel <= slave_sel + 1;
|
||||||
|
end
|
||||||
|
end else begin
|
||||||
|
// No active request in progress, so check if there's a new request on
|
||||||
|
// the selected slave.
|
||||||
|
if (req_valid[slave_sel]) begin
|
||||||
|
req_active <= '1;
|
||||||
|
req_load_output <= '1;
|
||||||
|
end else begin
|
||||||
|
// Go to next slave immediately
|
||||||
|
if(PRIORITY == 1)
|
||||||
|
slave_sel <= one_hot_to_binary(next_slave_one_hot);
|
||||||
|
// Round robin - Nothing from this slave, so move to the next slave.
|
||||||
|
else if (slave_sel == NUM_MASTERS-1)
|
||||||
|
slave_sel <= '0;
|
||||||
|
else
|
||||||
|
slave_sel <= slave_sel + 1;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
// Output Register
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// Here we load the active request for a single clock cycle and demultiplex
|
||||||
|
// the response back to the requesting master.
|
||||||
|
//
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
always_ff @(posedge m_ctrlport.clk) begin
|
||||||
|
// Load the active request
|
||||||
|
if (req_load_output) begin
|
||||||
|
m_ctrlport.req <= req_buffer[slave_sel];
|
||||||
|
end else begin
|
||||||
|
m_ctrlport.req.wr <= '0;
|
||||||
|
m_ctrlport.req.rd <= '0;
|
||||||
|
end
|
||||||
|
|
||||||
|
if (m_ctrlport.rst) begin
|
||||||
|
// only reset the flags of the master interface
|
||||||
|
m_ctrlport.req.wr <= '0;
|
||||||
|
m_ctrlport.req.rd <= '0;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
// Output any response to the master that made the request
|
||||||
|
for (genvar i = 0; i < NUM_MASTERS; i++) begin : gen_output_regs
|
||||||
|
always_ff @(posedge m_ctrlport.clk) begin
|
||||||
|
// Give the response data to all the slaves (no demux, to save logic)
|
||||||
|
s_ctrlport[i].resp <= m_ctrlport.resp;
|
||||||
|
|
||||||
|
// Give the ack only to the master that made the request (use a demux)
|
||||||
|
if (m_ctrlport.rst) begin
|
||||||
|
s_ctrlport[i].resp.ack <= '0;
|
||||||
|
end else if (i == slave_sel && m_ctrlport.resp.ack) begin
|
||||||
|
s_ctrlport[i].resp.ack <= '1;
|
||||||
|
end else begin
|
||||||
|
s_ctrlport[i].resp.ack <= '0;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
// optional watchdog
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
if (TIMEOUT == 0) begin: gen_no_timeout
|
||||||
|
// never set a timeout but in always_ff block to avoid error with synchronous assignment
|
||||||
|
// in gen_timeout block below
|
||||||
|
always_ff @(posedge m_ctrlport.clk) begin
|
||||||
|
timeout_occurred = '0;
|
||||||
|
end
|
||||||
|
|
||||||
|
end else begin: gen_timeout
|
||||||
|
// When the timeout occurred it takes 2 more clock cycles for the counter to reach zero
|
||||||
|
// The path is: timeout_occurred -> req_active = 0 -> timeout_counter = 0
|
||||||
|
// As the timeout counter has a minimum width of 2 bits the counter cannot reach TIMEOUT+1
|
||||||
|
// again within these two clock cycles.
|
||||||
|
// Therefore no other reset other than req_active is needed on this signal.
|
||||||
|
logic [$clog2(TIMEOUT+2):0] timeout_counter = '0;
|
||||||
|
|
||||||
|
// Reset the timeout counter when there is no active request
|
||||||
|
always_ff @(posedge m_ctrlport.clk) begin
|
||||||
|
if (~req_active) begin
|
||||||
|
timeout_counter <= '0;
|
||||||
|
end else begin
|
||||||
|
timeout_counter <= timeout_counter + 1;
|
||||||
|
end
|
||||||
|
|
||||||
|
// Set the timeout flag for one cycle when the counter reaches the timeout value.
|
||||||
|
// The delay of the additional register compensates the delay of the logic above to
|
||||||
|
// load the active request into the output register.
|
||||||
|
timeout_occurred <= timeout_counter == TIMEOUT;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
endmodule
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2025 Ettus Research, a National Instruments Brand
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
//
|
||||||
|
// Module: ctrlport_if_decoder
|
||||||
|
//
|
||||||
|
// Description:
|
||||||
|
//
|
||||||
|
// This block splits a single control port interface into multiple. It is
|
||||||
|
// used when you have a single master that needs to access multiple slaves.
|
||||||
|
//
|
||||||
|
// This version also implements address decoding. The request is passed to a
|
||||||
|
// slave only if the address falls within that slave's address space. Each
|
||||||
|
// slave can have a unique base address and address space size.
|
||||||
|
//
|
||||||
|
// When passed to the slave, the base address is subtracted from the request
|
||||||
|
// address.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
//
|
||||||
|
// NUM_SLAVES : The number of slaves to connect to a master.
|
||||||
|
// PORT_BASE : Base addresses to use for each slave.
|
||||||
|
// PORT_SIZE : Size of the address space for each slave.
|
||||||
|
//
|
||||||
|
|
||||||
|
module ctrlport_if_decoder #(
|
||||||
|
int NUM_SLAVES = 2,
|
||||||
|
int PORT_BASE [NUM_SLAVES] = '{'h0, 'h100},
|
||||||
|
int PORT_SIZE [NUM_SLAVES] = '{'h100, 'h100}
|
||||||
|
) (
|
||||||
|
// Slave Interface
|
||||||
|
ctrlport_if.slave s_ctrlport,
|
||||||
|
// Master Interfaces
|
||||||
|
ctrlport_if.master m_ctrlport [NUM_SLAVES]
|
||||||
|
);
|
||||||
|
|
||||||
|
import ctrlport_pkg::*;
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
// Check the address ranges
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
for (genvar i = 0; i < NUM_SLAVES; i = i+1) begin : gen_overlap_1
|
||||||
|
for (genvar j = 0; j < NUM_SLAVES; j = j+1) begin : gen_overlap_2
|
||||||
|
if (i != j) begin
|
||||||
|
if ((PORT_BASE[i] >= PORT_BASE[j]) &&
|
||||||
|
(PORT_BASE[i] < PORT_BASE[j] + PORT_SIZE[j])) begin
|
||||||
|
$error("Port %0d overlaps with port %0d.", i, j);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
// Split the requests among the slaves
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
for (genvar i = 0; i < NUM_SLAVES; i = i+1) begin : gen_split
|
||||||
|
always_ff @(posedge s_ctrlport.clk) begin
|
||||||
|
// unconditionally pass the request by default
|
||||||
|
m_ctrlport[i].req <= s_ctrlport.req;
|
||||||
|
// pass only the respective address bits
|
||||||
|
m_ctrlport[i].req.addr <= '0;
|
||||||
|
m_ctrlport[i].req.addr[$clog2(PORT_SIZE[i])-1:0] <= s_ctrlport.req.addr - PORT_BASE[i];
|
||||||
|
|
||||||
|
// read and write trigger transactions and therefore need to react to reset
|
||||||
|
if (s_ctrlport.rst) begin
|
||||||
|
m_ctrlport[i].req.wr <= 1'b0;
|
||||||
|
m_ctrlport[i].req.rd <= 1'b0;
|
||||||
|
end else begin
|
||||||
|
automatic logic address_in_range;
|
||||||
|
address_in_range = (s_ctrlport.req.addr >= PORT_BASE[i]) &&
|
||||||
|
(s_ctrlport.req.addr < PORT_BASE[i] + PORT_SIZE[i]);
|
||||||
|
m_ctrlport[i].req.wr <= s_ctrlport.req.wr & address_in_range;
|
||||||
|
m_ctrlport[i].req.rd <= s_ctrlport.req.rd & address_in_range;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
// Decode the responses
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
// Take the responses and mask them with their respective ack
|
||||||
|
ctrlport_response_t masked_resp [NUM_SLAVES-1:0];
|
||||||
|
for (genvar i = 0; i < NUM_SLAVES; i++) begin : gen_mask
|
||||||
|
assign masked_resp[i] = m_ctrlport[i].resp.ack ? m_ctrlport[i].resp : '0;
|
||||||
|
end
|
||||||
|
|
||||||
|
// Combine the masked responses by OR'ing them together
|
||||||
|
ctrlport_response_t combined_resp;
|
||||||
|
always_comb begin : response_combine
|
||||||
|
combined_resp = '0;
|
||||||
|
for (int i = 0; i < NUM_SLAVES; i++) begin : gen_or
|
||||||
|
combined_resp = combined_resp | masked_resp[i];
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
// Register the output to break combinatorial path
|
||||||
|
always_ff @(posedge s_ctrlport.clk) begin : response_reg
|
||||||
|
s_ctrlport.resp <= combined_resp;
|
||||||
|
|
||||||
|
if (s_ctrlport.rst) begin
|
||||||
|
s_ctrlport.resp.ack <= '0;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
endmodule
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2025 Ettus Research, a National Instruments Brand
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
//
|
||||||
|
// Module: ctrlport_if_splitter
|
||||||
|
//
|
||||||
|
// Description:
|
||||||
|
//
|
||||||
|
// This block splits a single control-port interface into multiple. It is used
|
||||||
|
// when you have a single master that needs to access multiple slaves. For
|
||||||
|
// example, a NoC block where the registers are implemented in multiple
|
||||||
|
// submodules that must be read/written by a single NoC shell.
|
||||||
|
//
|
||||||
|
// Note that this block does not do any address decoding, so the connected
|
||||||
|
// slaves must use non-overlapping address spaces.
|
||||||
|
//
|
||||||
|
// This module takes the request received by its single slave interface and
|
||||||
|
// outputs it on all its master interfaces. In the opposite direction, it takes
|
||||||
|
// the responses received by its multiple master interfaces and combines them
|
||||||
|
// into a single response on its slave interface. This is done by using the ack
|
||||||
|
// bit of each response to mask the other bits of the response, then OR'ing all
|
||||||
|
// of the masked responses together onto a single response bus. This is valid
|
||||||
|
// because only one block is allowed to respond to a single request.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
//
|
||||||
|
// NUM_SLAVES : The number of slaves you want to connect to a master.
|
||||||
|
//
|
||||||
|
|
||||||
|
|
||||||
|
module ctrlport_if_splitter #(
|
||||||
|
int NUM_SLAVES = 2
|
||||||
|
) (
|
||||||
|
// Slave Interface
|
||||||
|
ctrlport_if.slave s_ctrlport,
|
||||||
|
// Master Interfaces
|
||||||
|
ctrlport_if.master m_ctrlport [NUM_SLAVES-1:0]
|
||||||
|
);
|
||||||
|
|
||||||
|
import ctrlport_pkg::*;
|
||||||
|
|
||||||
|
generate
|
||||||
|
if (NUM_SLAVES == 1) begin : gen_no_split
|
||||||
|
assign m_ctrlport[0].req = s_ctrlport.req;
|
||||||
|
assign s_ctrlport.resp = m_ctrlport[0].resp;
|
||||||
|
end else begin : gen_splitter
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
// Split the requests among the slaves
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
for (genvar i = 0; i < NUM_SLAVES; i++) begin : gen_split
|
||||||
|
// No special logic is required to split the requests from the master among
|
||||||
|
// multiple slaves.
|
||||||
|
assign m_ctrlport[i].req = s_ctrlport.req;
|
||||||
|
end
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
// Decode the responses
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
// Take the responses and mask them with their respective ack
|
||||||
|
ctrlport_response_t masked_resp [NUM_SLAVES-1:0];
|
||||||
|
for (genvar i = 0; i < NUM_SLAVES; i++) begin : gen_mask
|
||||||
|
assign masked_resp[i] = m_ctrlport[i].resp.ack ? m_ctrlport[i].resp : '0;
|
||||||
|
end
|
||||||
|
|
||||||
|
// Combine the masked responses by OR'ing them together
|
||||||
|
ctrlport_response_t combined_resp;
|
||||||
|
always_comb begin : response_combine
|
||||||
|
combined_resp = '0;
|
||||||
|
for (int i = 0; i < NUM_SLAVES; i++) begin : gen_or
|
||||||
|
combined_resp = combined_resp | masked_resp[i];
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
// Register the output to break combinatorial path
|
||||||
|
always_ff @(posedge s_ctrlport.clk) begin : response_reg
|
||||||
|
s_ctrlport.resp <= combined_resp;
|
||||||
|
|
||||||
|
if (s_ctrlport.rst) begin
|
||||||
|
s_ctrlport.resp.ack <= '0;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
endgenerate
|
||||||
|
endmodule
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2025 Ettus Research, a National Instruments Brand
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
//
|
||||||
|
// Module: ctrlport_if_terminator
|
||||||
|
//
|
||||||
|
// Description:
|
||||||
|
//
|
||||||
|
// Returns an error for all ctrlport requests in given address range.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
//
|
||||||
|
// BASE_ADDRESS : First address to generate response for.
|
||||||
|
// WINDOW_SIZE : Size of the address space.
|
||||||
|
// Last address in range will be BASE_ADDRESS + WINDOW_SIZE - 1.
|
||||||
|
//
|
||||||
|
|
||||||
|
module ctrlport_if_terminator #(
|
||||||
|
int BASE_ADDRESS = 0,
|
||||||
|
int WINDOW_SIZE = 32
|
||||||
|
)(
|
||||||
|
ctrlport_if.slave s_ctrlport
|
||||||
|
);
|
||||||
|
|
||||||
|
import ctrlport_pkg::*;
|
||||||
|
|
||||||
|
// drive acknowledgement on requests but not on reset
|
||||||
|
always_ff @(posedge s_ctrlport.clk) begin
|
||||||
|
if (s_ctrlport.rst) begin
|
||||||
|
s_ctrlport.resp.ack <= 1'b0;
|
||||||
|
end else if ((s_ctrlport.req.addr >= BASE_ADDRESS) &&
|
||||||
|
(s_ctrlport.req.addr < BASE_ADDRESS + WINDOW_SIZE)) begin
|
||||||
|
s_ctrlport.resp.ack <= s_ctrlport.req.wr | s_ctrlport.req.rd;
|
||||||
|
end else begin
|
||||||
|
s_ctrlport.resp.ack <= 1'b0;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
// other outputs are fixed
|
||||||
|
assign s_ctrlport.resp.status = STS_CMDERR;
|
||||||
|
assign s_ctrlport.resp.data = '0;
|
||||||
|
|
||||||
|
endmodule
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2025 Ettus Research, a National Instruments Brand
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
//
|
||||||
|
// Module: ctrlport_if_window
|
||||||
|
//
|
||||||
|
// Description:
|
||||||
|
//
|
||||||
|
// Copy requests from slave to master interface when s_ctrlport_req_addr is in
|
||||||
|
// address range specified by the parameters.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
//
|
||||||
|
// BASE_ADDRESS : Base address of the window.
|
||||||
|
// This will be the first address in the range.
|
||||||
|
// WINDOW_SIZE : Size of the address space for the window.
|
||||||
|
// Last address in range will be BASE_ADDRESS + WINDOW_SIZE - 1.
|
||||||
|
//
|
||||||
|
|
||||||
|
module ctrlport_if_window #(
|
||||||
|
int BASE_ADDRESS = 0,
|
||||||
|
int WINDOW_SIZE = 32
|
||||||
|
) (
|
||||||
|
ctrlport_if.slave s_ctrlport,
|
||||||
|
ctrlport_if.master m_ctrlport
|
||||||
|
);
|
||||||
|
|
||||||
|
always_comb begin
|
||||||
|
// Mask write and read flag
|
||||||
|
automatic logic address_in_range = (s_ctrlport.req.addr >= BASE_ADDRESS) &&
|
||||||
|
(s_ctrlport.req.addr < BASE_ADDRESS + WINDOW_SIZE);
|
||||||
|
|
||||||
|
// initialize master request by slave request
|
||||||
|
m_ctrlport.req = s_ctrlport.req;
|
||||||
|
|
||||||
|
// overwrite write and read flag with in range flag
|
||||||
|
m_ctrlport.req.wr = s_ctrlport.req.wr && address_in_range;
|
||||||
|
m_ctrlport.req.rd = s_ctrlport.req.rd && address_in_range;
|
||||||
|
|
||||||
|
// transfer reponse without change
|
||||||
|
s_ctrlport.resp = m_ctrlport.resp;
|
||||||
|
end
|
||||||
|
|
||||||
|
endmodule
|
||||||
@@ -5,17 +5,18 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
##################################################
|
##################################################
|
||||||
# Dependencies for AXI BFMS
|
# Dependencies for BFMs
|
||||||
##################################################
|
##################################################
|
||||||
|
|
||||||
SIM_RFNOC_SRCS = $(abspath $(addprefix $(BASE_DIR)/../lib/, \
|
SIM_RFNOC_SRCS = $(abspath $(addprefix $(BASE_DIR)/../lib/, \
|
||||||
axi4lite_sv/PkgAxiLite.sv \
|
axi4lite_sv/PkgAxiLite.sv \
|
||||||
axi4lite_sv/AxiLiteIf.sv \
|
axi4lite_sv/AxiLiteIf.sv \
|
||||||
axi4s_sv/AxiStreamIf.sv \
|
axi4s_sv/AxiStreamIf.sv \
|
||||||
|
rfnoc/core/ctrlport_pkg.sv \
|
||||||
))
|
))
|
||||||
|
|
||||||
##################################################
|
##################################################
|
||||||
# Simulation Libraries/Headers for AXI based interfaces
|
# Simulation Libraries/Headers for various interfaces
|
||||||
##################################################
|
##################################################
|
||||||
|
|
||||||
SIM_RFNOC_SRCS += $(abspath $(addprefix $(BASE_DIR)/../sim/rfnoc/, \
|
SIM_RFNOC_SRCS += $(abspath $(addprefix $(BASE_DIR)/../sim/rfnoc/, \
|
||||||
@@ -33,4 +34,5 @@ PkgRfnocItemUtils.sv \
|
|||||||
PkgCtrlIfaceBfm.sv \
|
PkgCtrlIfaceBfm.sv \
|
||||||
PkgChdrIfaceBfm.sv \
|
PkgChdrIfaceBfm.sv \
|
||||||
PkgRfnocBlockCtrlBfm.sv \
|
PkgRfnocBlockCtrlBfm.sv \
|
||||||
|
ctrlport_bfm_pkg.sv \
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2024 Ettus Research, a National Instruments Brand
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
//
|
||||||
|
// Package: ctrlport_bfm_pkg
|
||||||
|
//
|
||||||
|
// Description:
|
||||||
|
//
|
||||||
|
// Package defining a Control Port bus functional model (BFM).
|
||||||
|
// See the RFNoC Specification for a protocol description.
|
||||||
|
//
|
||||||
|
|
||||||
|
package ctrlport_bfm_pkg;
|
||||||
|
|
||||||
|
import ctrlport_pkg::*;
|
||||||
|
|
||||||
|
class ctrlport_bfm;
|
||||||
|
|
||||||
|
// create combined request and response struct for mailbox
|
||||||
|
typedef struct {
|
||||||
|
ctrlport_request_t req;
|
||||||
|
ctrlport_response_t resp;
|
||||||
|
bit skip_data_check;
|
||||||
|
} ctrlport_transfer_t;
|
||||||
|
|
||||||
|
local mailbox #(ctrlport_transfer_t) transactions;
|
||||||
|
|
||||||
|
// link to interface
|
||||||
|
local virtual ctrlport_if iface;
|
||||||
|
|
||||||
|
// data copy for response data
|
||||||
|
local logic [CTRLPORT_DATA_W-1:0] reponse_data;
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Constructor
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Class constructor. This must be given an interface for the master
|
||||||
|
// connection and an interface for the slave connection.
|
||||||
|
function new(virtual ctrlport_if iface);
|
||||||
|
this.iface = iface;
|
||||||
|
transactions = new;
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Asynchronous transaction handling
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Queue an asynchronous transaction (read or write with full request)
|
||||||
|
task async_request(
|
||||||
|
ctrlport_request_t request,
|
||||||
|
ctrlport_response_t expected_response,
|
||||||
|
bit skip_data_check = '0
|
||||||
|
);
|
||||||
|
assert (request.rd || request.wr) else $error(1, "Transaction must be read or write");
|
||||||
|
transactions.put('{request, expected_response, skip_data_check});
|
||||||
|
endtask
|
||||||
|
|
||||||
|
// Queue a simple write transaction
|
||||||
|
task async_write(
|
||||||
|
logic[CTRLPORT_ADDR_W-1:0] addr,
|
||||||
|
logic[CTRLPORT_DATA_W-1:0] data,
|
||||||
|
ctrlport_status_t expected_status = STS_OKAY
|
||||||
|
);
|
||||||
|
ctrlport_request_t request;
|
||||||
|
ctrlport_response_t response;
|
||||||
|
|
||||||
|
request = '0;
|
||||||
|
request.wr = '1;
|
||||||
|
request.addr = addr;
|
||||||
|
request.data = data;
|
||||||
|
|
||||||
|
response = '0;
|
||||||
|
response.status = expected_status;
|
||||||
|
|
||||||
|
async_request(request, response);
|
||||||
|
endtask;
|
||||||
|
|
||||||
|
// Queue a simple read transaction with automated response checking
|
||||||
|
task async_read(
|
||||||
|
logic [CTRLPORT_ADDR_W-1:0] addr,
|
||||||
|
logic [CTRLPORT_DATA_W-1:0] expected_data,
|
||||||
|
ctrlport_status_t expected_status = STS_OKAY
|
||||||
|
);
|
||||||
|
ctrlport_request_t request;
|
||||||
|
ctrlport_response_t response;
|
||||||
|
|
||||||
|
request = '0;
|
||||||
|
request.rd = '1;
|
||||||
|
request.addr = addr;
|
||||||
|
|
||||||
|
response = '0;
|
||||||
|
response.status = expected_status;
|
||||||
|
response.data = expected_data;
|
||||||
|
|
||||||
|
async_request(request, response);
|
||||||
|
endtask;
|
||||||
|
|
||||||
|
// wait for all transactions to be processed
|
||||||
|
task wait_complete();
|
||||||
|
while(transactions.num() > 0) begin
|
||||||
|
@(posedge iface.clk);
|
||||||
|
end
|
||||||
|
endtask
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Synchronous transaction handling
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Perform a request and get response
|
||||||
|
task request(
|
||||||
|
input ctrlport_request_t request,
|
||||||
|
input ctrlport_response_t expected_response,
|
||||||
|
output logic[CTRLPORT_DATA_W-1:0] data
|
||||||
|
);
|
||||||
|
async_request(request, expected_response);
|
||||||
|
wait_complete();
|
||||||
|
data = reponse_data;
|
||||||
|
endtask;
|
||||||
|
|
||||||
|
// Perform a simple write transaction
|
||||||
|
task write(
|
||||||
|
logic[CTRLPORT_ADDR_W-1:0] addr,
|
||||||
|
logic[CTRLPORT_DATA_W-1:0] data,
|
||||||
|
ctrlport_status_t expected_status = STS_OKAY
|
||||||
|
);
|
||||||
|
async_write(addr, data, expected_status);
|
||||||
|
wait_complete();
|
||||||
|
endtask;
|
||||||
|
|
||||||
|
// Perform a simple read transaction, no response data check, return data from interface
|
||||||
|
task read(
|
||||||
|
input logic[CTRLPORT_ADDR_W-1:0] addr,
|
||||||
|
output logic[CTRLPORT_DATA_W-1:0] data,
|
||||||
|
input ctrlport_status_t expected_status = STS_OKAY
|
||||||
|
);
|
||||||
|
ctrlport_request_t request;
|
||||||
|
ctrlport_response_t response;
|
||||||
|
|
||||||
|
request = '0;
|
||||||
|
request.rd = '1;
|
||||||
|
request.addr = addr;
|
||||||
|
|
||||||
|
response = '0;
|
||||||
|
response.status = expected_status;
|
||||||
|
|
||||||
|
async_request(request, response, '1);
|
||||||
|
wait_complete();
|
||||||
|
data = reponse_data;
|
||||||
|
endtask;
|
||||||
|
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Signal handling
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Start the BFM.
|
||||||
|
// This will join a separate thread for the interface handling.
|
||||||
|
task run();
|
||||||
|
fork
|
||||||
|
request_body();
|
||||||
|
join_none
|
||||||
|
endtask
|
||||||
|
|
||||||
|
// Control the master request interface
|
||||||
|
local task request_body();
|
||||||
|
ctrlport_transfer_t transfer;
|
||||||
|
ctrlport_request_t request_idle;
|
||||||
|
|
||||||
|
// initialize request interface
|
||||||
|
request_idle = 'x;
|
||||||
|
request_idle.wr = '0;
|
||||||
|
request_idle.rd = '0;
|
||||||
|
iface.req = request_idle;
|
||||||
|
|
||||||
|
// ignore first clock cycle to let response settle
|
||||||
|
@(posedge iface.clk);
|
||||||
|
|
||||||
|
forever begin
|
||||||
|
// handle any available transaction (but leave it in the mailbox for now)
|
||||||
|
if (transactions.try_peek(transfer)) begin
|
||||||
|
// wait for reset to be deasserted
|
||||||
|
while(iface.rst) begin
|
||||||
|
@(posedge iface.clk);
|
||||||
|
check_no_response();
|
||||||
|
end
|
||||||
|
|
||||||
|
// send request
|
||||||
|
iface.req = transfer.req;
|
||||||
|
// check minimum time for response on interface
|
||||||
|
@(negedge iface.clk);
|
||||||
|
check_no_response();
|
||||||
|
|
||||||
|
// reset request interface
|
||||||
|
@(posedge iface.clk);
|
||||||
|
iface.req = request_idle;
|
||||||
|
|
||||||
|
// wait for response
|
||||||
|
while(!iface.resp.ack) begin
|
||||||
|
@(posedge iface.clk);
|
||||||
|
end
|
||||||
|
|
||||||
|
// status is always expected to match
|
||||||
|
assert (iface.resp.status == transfer.resp.status) else
|
||||||
|
$error(1, "Unexpected status received on interface. Expected: %2b Received: %2b", transfer.resp.status, iface.resp.status);
|
||||||
|
|
||||||
|
// check data if read and data check is not skipped
|
||||||
|
if (transfer.req.rd && !transfer.skip_data_check) begin
|
||||||
|
assert (iface.resp.data == transfer.resp.data) else
|
||||||
|
$error(1, "Unexpected data received on interface. Expected: %h Received: %h", transfer.resp.data, iface.resp.data);
|
||||||
|
end
|
||||||
|
|
||||||
|
// save data for read operations
|
||||||
|
reponse_data = iface.resp.data;
|
||||||
|
|
||||||
|
// request is done -> remove from queue
|
||||||
|
transactions.get(transfer);
|
||||||
|
|
||||||
|
// wait at least one clock cycle for next check of mailbox
|
||||||
|
end else begin
|
||||||
|
@(posedge iface.clk);
|
||||||
|
check_no_response();
|
||||||
|
end
|
||||||
|
end
|
||||||
|
endtask
|
||||||
|
|
||||||
|
// Check for no response in this clock cycle
|
||||||
|
local task check_no_response();
|
||||||
|
assert (iface.resp.ack == '0) else $error(1, "Unexpected response received on interface: %p", iface.resp);
|
||||||
|
endtask
|
||||||
|
|
||||||
|
endclass
|
||||||
|
|
||||||
|
endpackage
|
||||||
Reference in New Issue
Block a user