Reorganize test dir to ensure test of installed pkg

This commit is contained in:
Alex Mykyta
2022-02-28 23:08:41 -08:00
parent a8bf3c5132
commit 54d783e1ab
138 changed files with 15 additions and 19 deletions

0
tests/lib/__init__.py Normal file
View File

121
tests/lib/base_testcase.py Normal file
View File

@@ -0,0 +1,121 @@
from typing import Optional, List
import unittest
import os
import glob
import shutil
import inspect
import pathlib
import pytest
from systemrdl import RDLCompiler
from peakrdl.regblock import RegblockExporter
from .cpuifs.base import CpuifTestMode
from .cpuifs.apb3 import APB3
class BaseTestCase(unittest.TestCase):
#: Path to the testcase's RDL file.
#: Relative to the testcase's dir. If unset, the first RDL file found in the
#: testcase dir will be used
rdl_file = None # type: Optional[str]
#: RDL type name to elaborate. If unset, compiler will automatically choose
#: the top.
rdl_elab_target = None # type: Optional[str]
#: Parameters to pass into RDL elaboration
rdl_elab_params = {}
#: Define what CPUIF to use for this testcase
cpuif = APB3() # type: CpuifTestMode
# Other exporter args:
retime_read_fanin = False
retime_read_response = False
#: this gets auto-loaded via the _load_request autouse fixture
request = None # type: pytest.FixtureRequest
exporter = RegblockExporter()
@pytest.fixture(autouse=True)
def _load_request(self, request):
self.request = request
@classmethod
def get_testcase_dir(cls) -> str:
class_dir = os.path.dirname(inspect.getfile(cls))
return class_dir
@classmethod
def get_run_dir(cls) -> str:
this_dir = cls.get_testcase_dir()
run_dir = os.path.join(this_dir, "run.out", cls.__name__)
return run_dir
@classmethod
def _write_params(cls) -> None:
"""
Write out the class parameters to a file so that it is easier to debug
how a testcase was parameterized
"""
path = os.path.join(cls.get_run_dir(), "params.txt")
with open(path, 'w') as f:
for k, v in cls.__dict__.items():
if k.startswith("_") or callable(v):
continue
f.write(f"{k}: {repr(v)}\n")
@classmethod
def _export_regblock(cls):
"""
Call the peakrdl.regblock exporter to generate the DUT
"""
this_dir = cls.get_testcase_dir()
if cls.rdl_file:
rdl_file = cls.rdl_file
else:
# Find any *.rdl file in testcase dir
rdl_file = glob.glob(os.path.join(this_dir, "*.rdl"))[0]
rdlc = RDLCompiler()
rdlc.compile_file(rdl_file)
root = rdlc.elaborate(cls.rdl_elab_target, "regblock", cls.rdl_elab_params)
cls.exporter.export(
root,
cls.get_run_dir(),
module_name="regblock",
package_name="regblock_pkg",
cpuif_cls=cls.cpuif.cpuif_cls,
retime_read_fanin=cls.retime_read_fanin,
retime_read_response=cls.retime_read_response,
)
@classmethod
def setUpClass(cls):
# Create fresh build dir
run_dir = cls.get_run_dir()
if os.path.exists(run_dir):
shutil.rmtree(run_dir)
pathlib.Path(run_dir).mkdir(parents=True, exist_ok=True)
cls._write_params()
# Convert testcase RDL file --> SV
cls._export_regblock()
def setUp(self) -> None:
# cd into the run directory
self.original_cwd = os.getcwd()
os.chdir(self.get_run_dir())
def run_test(self, plusargs:List[str] = None) -> None:
simulator = self.simulator_cls(testcase_cls_inst=self)
simulator.run(plusargs)

View File

View File

@@ -0,0 +1,18 @@
from ..base import CpuifTestMode
from peakrdl.regblock.cpuif.apb3 import APB3_Cpuif, APB3_Cpuif_flattened
class APB3(CpuifTestMode):
cpuif_cls = APB3_Cpuif
rtl_files = [
"apb3_intf.sv",
]
tb_files = [
"apb3_intf.sv",
"apb3_intf_driver.sv",
]
tb_template = "tb_inst.sv"
class FlatAPB3(APB3):
cpuif_cls = APB3_Cpuif_flattened
rtl_files = []

View File

@@ -0,0 +1,40 @@
interface apb3_intf #(
parameter DATA_WIDTH = 32,
parameter ADDR_WIDTH = 32
);
// Command
logic PSEL;
logic PENABLE;
logic PWRITE;
logic [ADDR_WIDTH-1:0] PADDR;
logic [DATA_WIDTH-1:0] PWDATA;
// Response
logic [DATA_WIDTH-1:0] PRDATA;
logic PREADY;
logic PSLVERR;
modport master (
output PSEL,
output PENABLE,
output PWRITE,
output PADDR,
output PWDATA,
input PRDATA,
input PREADY,
input PSLVERR
);
modport slave (
input PSEL,
input PENABLE,
input PWRITE,
input PADDR,
input PWDATA,
output PRDATA,
output PREADY,
output PSLVERR
);
endinterface

View File

@@ -0,0 +1,116 @@
interface apb3_intf_driver #(
parameter DATA_WIDTH = 32,
parameter ADDR_WIDTH = 32
)(
input wire clk,
input wire rst,
apb3_intf.master m_apb
);
timeunit 1ps;
timeprecision 1ps;
logic PSEL;
logic PENABLE;
logic PWRITE;
logic [ADDR_WIDTH-1:0] PADDR;
logic [DATA_WIDTH-1:0] PWDATA;
logic [DATA_WIDTH-1:0] PRDATA;
logic PREADY;
logic PSLVERR;
assign m_apb.PSEL = PSEL;
assign m_apb.PENABLE = PENABLE;
assign m_apb.PWRITE = PWRITE;
assign m_apb.PADDR = PADDR;
assign m_apb.PWDATA = PWDATA;
assign PRDATA = m_apb.PRDATA;
assign PREADY = m_apb.PREADY;
assign PSLVERR = m_apb.PSLVERR;
default clocking cb @(posedge clk);
default input #1step output #1;
output PSEL;
output PENABLE;
output PWRITE;
output PADDR;
output PWDATA;
input PRDATA;
input PREADY;
input PSLVERR;
endclocking
task automatic reset();
cb.PSEL <= '0;
cb.PENABLE <= '0;
cb.PWRITE <= '0;
cb.PADDR <= '0;
cb.PWDATA <= '0;
endtask
semaphore txn_mutex = new(1);
task automatic write(logic [ADDR_WIDTH-1:0] addr, logic [DATA_WIDTH-1:0] data);
txn_mutex.get();
##0;
// Initiate transfer
cb.PSEL <= '1;
cb.PENABLE <= '0;
cb.PWRITE <= '1;
cb.PADDR <= addr;
cb.PWDATA <= data;
@(cb);
// active phase
cb.PENABLE <= '1;
@(cb);
// Wait for response
while(cb.PREADY !== 1'b1) @(cb);
reset();
txn_mutex.put();
endtask
task automatic read(logic [ADDR_WIDTH-1:0] addr, output logic [DATA_WIDTH-1:0] data);
txn_mutex.get();
##0;
// Initiate transfer
cb.PSEL <= '1;
cb.PENABLE <= '0;
cb.PWRITE <= '0;
cb.PADDR <= addr;
cb.PWDATA <= '0;
@(cb);
// active phase
cb.PENABLE <= '1;
@(cb);
// Wait for response
while(cb.PREADY !== 1'b1) @(cb);
assert(!$isunknown(cb.PRDATA)) else $error("Read from 0x%0x returned X's on PRDATA", addr);
assert(!$isunknown(cb.PSLVERR)) else $error("Read from 0x%0x returned X's on PSLVERR", addr);
data = cb.PRDATA;
reset();
txn_mutex.put();
endtask
task automatic assert_read(logic [ADDR_WIDTH-1:0] addr, logic [DATA_WIDTH-1:0] expected_data, logic [DATA_WIDTH-1:0] mask = '1);
logic [DATA_WIDTH-1:0] data;
read(addr, data);
data &= mask;
assert(data == expected_data) else $error("Read from 0x%x returned 0x%x. Expected 0x%x", addr, data, expected_data);
endtask
initial begin
reset();
end
initial forever begin
@cb;
if(!rst) assert(!$isunknown(cb.PREADY)) else $error("Saw X on PREADY!");
end
endinterface

View File

@@ -0,0 +1,32 @@
{% sv_line_anchor %}
apb3_intf #(
.DATA_WIDTH({{exporter.cpuif.data_width}}),
.ADDR_WIDTH({{exporter.cpuif.addr_width}})
) s_apb();
apb3_intf_driver #(
.DATA_WIDTH({{exporter.cpuif.data_width}}),
.ADDR_WIDTH({{exporter.cpuif.addr_width}})
) cpuif (
.clk(clk),
.rst(rst),
.m_apb(s_apb)
);
{% if type(cpuif).__name__.startswith("Flat") %}
{% sv_line_anchor %}
wire s_apb_psel;
wire s_apb_penable;
wire s_apb_pwrite;
wire [{{exporter.cpuif.addr_width - 1}}:0] s_apb_paddr;
wire [{{exporter.cpuif.data_width - 1}}:0] s_apb_pwdata;
wire s_apb_pready;
wire [{{exporter.cpuif.data_width - 1}}:0] s_apb_prdata;
wire s_apb_pslverr;
assign s_apb_psel = s_apb.PSEL;
assign s_apb_penable = s_apb.PENABLE;
assign s_apb_pwrite = s_apb.PWRITE;
assign s_apb_paddr = s_apb.PADDR;
assign s_apb_pwdata = s_apb.PWDATA;
assign s_apb.PREADY = s_apb_pready;
assign s_apb.PRDATA = s_apb_prdata;
assign s_apb.PSLVERR = s_apb_pslverr;
{% endif %}

View File

@@ -0,0 +1,18 @@
from ..base import CpuifTestMode
from peakrdl.regblock.cpuif.axi4lite import AXI4Lite_Cpuif, AXI4Lite_Cpuif_flattened
class AXI4Lite(CpuifTestMode):
cpuif_cls = AXI4Lite_Cpuif
rtl_files = [
"axi4lite_intf.sv",
]
tb_files = [
"axi4lite_intf.sv",
"axi4lite_intf_driver.sv",
]
tb_template = "tb_inst.sv"
class FlatAXI4Lite(AXI4Lite):
cpuif_cls = AXI4Lite_Cpuif_flattened
rtl_files = []

View File

@@ -0,0 +1,80 @@
interface axi4lite_intf #(
parameter DATA_WIDTH = 32,
parameter ADDR_WIDTH = 32
);
logic AWREADY;
logic AWVALID;
logic [ADDR_WIDTH-1:0] AWADDR;
logic [2:0] AWPROT;
logic WREADY;
logic WVALID;
logic [DATA_WIDTH-1:0] WDATA;
logic [DATA_WIDTH/8-1:0] WSTRB;
logic BREADY;
logic BVALID;
logic [1:0] BRESP;
logic ARREADY;
logic ARVALID;
logic [ADDR_WIDTH-1:0] ARADDR;
logic [2:0] ARPROT;
logic RREADY;
logic RVALID;
logic [DATA_WIDTH-1:0] RDATA;
logic [1:0] RRESP;
modport master (
input AWREADY,
output AWVALID,
output AWADDR,
output AWPROT,
input WREADY,
output WVALID,
output WDATA,
output WSTRB,
output BREADY,
input BVALID,
input BRESP,
input ARREADY,
output ARVALID,
output ARADDR,
output ARPROT,
output RREADY,
input RVALID,
input RDATA,
input RRESP
);
modport slave (
output AWREADY,
input AWVALID,
input AWADDR,
input AWPROT,
output WREADY,
input WVALID,
input WDATA,
input WSTRB,
input BREADY,
output BVALID,
output BRESP,
output ARREADY,
input ARVALID,
input ARADDR,
input ARPROT,
input RREADY,
output RVALID,
output RDATA,
output RRESP
);
endinterface

View File

@@ -0,0 +1,193 @@
interface axi4lite_intf_driver #(
parameter DATA_WIDTH = 32,
parameter ADDR_WIDTH = 32
)(
input wire clk,
input wire rst,
axi4lite_intf.master m_axil
);
timeunit 1ps;
timeprecision 1ps;
logic AWREADY;
logic AWVALID;
logic [ADDR_WIDTH-1:0] AWADDR;
logic [2:0] AWPROT;
logic WREADY;
logic WVALID;
logic [DATA_WIDTH-1:0] WDATA;
logic [DATA_WIDTH/8-1:0] WSTRB;
logic BREADY;
logic BVALID;
logic [1:0] BRESP;
logic ARREADY;
logic ARVALID;
logic [ADDR_WIDTH-1:0] ARADDR;
logic [2:0] ARPROT;
logic RREADY;
logic RVALID;
logic [DATA_WIDTH-1:0] RDATA;
logic [1:0] RRESP;
assign AWREADY = m_axil.AWREADY;
assign m_axil.AWVALID = AWVALID;
assign m_axil.AWADDR = AWADDR;
assign m_axil.AWPROT = AWPROT;
assign WREADY = m_axil.WREADY;
assign m_axil.WVALID = WVALID;
assign m_axil.WDATA = WDATA;
assign m_axil.WSTRB = WSTRB;
assign m_axil.BREADY = BREADY;
assign BVALID = m_axil.BVALID;
assign BRESP = m_axil.BRESP;
assign ARREADY = m_axil.ARREADY;
assign m_axil.ARVALID = ARVALID;
assign m_axil.ARADDR = ARADDR;
assign m_axil.ARPROT = ARPROT;
assign m_axil.RREADY = RREADY;
assign RVALID = m_axil.RVALID;
assign RDATA = m_axil.RDATA;
assign RRESP = m_axil.RRESP;
default clocking cb @(posedge clk);
default input #1step output #1;
input AWREADY;
output AWVALID;
output AWADDR;
output AWPROT;
input WREADY;
output WVALID;
output WDATA;
output WSTRB;
inout BREADY;
input BVALID;
input BRESP;
input ARREADY;
output ARVALID;
output ARADDR;
output ARPROT;
inout RREADY;
input RVALID;
input RDATA;
input RRESP;
endclocking
task automatic reset();
cb.AWVALID <= '0;
cb.AWADDR <= '0;
cb.AWPROT <= '0;
cb.WVALID <= '0;
cb.WDATA <= '0;
cb.WSTRB <= '0;
cb.ARVALID <= '0;
cb.ARADDR <= '0;
cb.ARPROT <= '0;
endtask
initial forever begin
cb.RREADY <= $urandom_range(1, 0);
cb.BREADY <= $urandom_range(1, 0);
@cb;
end
semaphore txn_aw_mutex = new(1);
semaphore txn_w_mutex = new(1);
semaphore txn_b_mutex = new(1);
semaphore txn_ar_mutex = new(1);
semaphore txn_r_mutex = new(1);
task automatic write(logic [ADDR_WIDTH-1:0] addr, logic [DATA_WIDTH-1:0] data);
bit w_before_aw;
w_before_aw = $urandom_range(1,0);
fork
begin
txn_aw_mutex.get();
##0;
if(w_before_aw) repeat($urandom_range(2,0)) @cb;
cb.AWVALID <= '1;
cb.AWADDR <= addr;
cb.AWPROT <= '0;
@(cb);
while(cb.AWREADY !== 1'b1) @(cb);
cb.AWVALID <= '0;
txn_aw_mutex.put();
end
begin
txn_w_mutex.get();
##0;
if(!w_before_aw) repeat($urandom_range(2,0)) @cb;
cb.WVALID <= '1;
cb.WDATA <= data;
cb.WSTRB <= '1; // TODO: Support byte strobes
@(cb);
while(cb.WREADY !== 1'b1) @(cb);
cb.WVALID <= '0;
cb.WSTRB <= '0;
txn_w_mutex.put();
end
begin
txn_b_mutex.get();
@cb;
while(!(cb.BREADY === 1'b1 && cb.BVALID === 1'b1)) @(cb);
assert(!$isunknown(cb.BRESP)) else $error("Read from 0x%0x returned X's on BRESP", addr);
txn_b_mutex.put();
end
join
endtask
task automatic read(logic [ADDR_WIDTH-1:0] addr, output logic [DATA_WIDTH-1:0] data);
fork
begin
txn_ar_mutex.get();
##0;
cb.ARVALID <= '1;
cb.ARADDR <= addr;
cb.ARPROT <= '0;
@(cb);
while(cb.ARREADY !== 1'b1) @(cb);
cb.ARVALID <= '0;
txn_ar_mutex.put();
end
begin
txn_r_mutex.get();
@cb;
while(!(cb.RREADY === 1'b1 && cb.RVALID === 1'b1)) @(cb);
assert(!$isunknown(cb.RDATA)) else $error("Read from 0x%0x returned X's on RDATA", addr);
assert(!$isunknown(cb.RRESP)) else $error("Read from 0x%0x returned X's on RRESP", addr);
data = cb.RDATA;
txn_r_mutex.put();
end
join
endtask
task automatic assert_read(logic [ADDR_WIDTH-1:0] addr, logic [DATA_WIDTH-1:0] expected_data, logic [DATA_WIDTH-1:0] mask = '1);
logic [DATA_WIDTH-1:0] data;
read(addr, data);
data &= mask;
assert(data == expected_data) else $error("Read from 0x%x returned 0x%x. Expected 0x%x", addr, data, expected_data);
endtask
initial begin
reset();
end
initial forever begin
@cb;
if(!rst) assert(!$isunknown(cb.AWREADY)) else $error("Saw X on AWREADY!");
if(!rst) assert(!$isunknown(cb.WREADY)) else $error("Saw X on WREADY!");
if(!rst) assert(!$isunknown(cb.BVALID)) else $error("Saw X on BVALID!");
if(!rst) assert(!$isunknown(cb.ARREADY)) else $error("Saw X on ARREADY!");
if(!rst) assert(!$isunknown(cb.RVALID)) else $error("Saw X on RVALID!");
end
endinterface

View File

@@ -0,0 +1,54 @@
{% sv_line_anchor %}
axi4lite_intf #(
.DATA_WIDTH({{exporter.cpuif.data_width}}),
.ADDR_WIDTH({{exporter.cpuif.addr_width}})
) s_axil();
axi4lite_intf_driver #(
.DATA_WIDTH({{exporter.cpuif.data_width}}),
.ADDR_WIDTH({{exporter.cpuif.addr_width}})
) cpuif (
.clk(clk),
.rst(rst),
.m_axil(s_axil)
);
{% if type(cpuif).__name__.startswith("Flat") %}
{% sv_line_anchor %}
wire s_axil_awready;
wire s_axil_awvalid;
wire [{{exporter.cpuif.addr_width - 1}}:0] s_axil_awaddr;
wire [2:0] s_axil_awprot;
wire s_axil_wready;
wire s_axil_wvalid;
wire [{{exporter.cpuif.data_width - 1}}:0] s_axil_wdata;
wire [{{exporter.cpuif.data_width_bytes - 1}}:0] s_axil_wstrb;
wire s_axil_bready;
wire s_axil_bvalid;
wire [1:0] s_axil_bresp;
wire s_axil_arready;
wire s_axil_arvalid;
wire [{{exporter.cpuif.addr_width - 1}}:0] s_axil_araddr;
wire [2:0] s_axil_arprot;
wire s_axil_rready;
wire s_axil_rvalid;
wire [{{exporter.cpuif.data_width - 1}}:0] s_axil_rdata;
wire [1:0] s_axil_rresp;
assign s_axil.AWREADY = s_axil_awready;
assign s_axil_awvalid = s_axil.AWVALID;
assign s_axil_awaddr = s_axil.AWADDR;
assign s_axil_awprot = s_axil.AWPROT;
assign s_axil.WREADY = s_axil_wready;
assign s_axil_wvalid = s_axil.WVALID;
assign s_axil_wdata = s_axil.WDATA;
assign s_axil_wstrb = s_axil.WSTRB;
assign s_axil_bready = s_axil.BREADY;
assign s_axil.BVALID = s_axil_bvalid;
assign s_axil.BRESP = s_axil_bresp;
assign s_axil.ARREADY = s_axil_arready;
assign s_axil_arvalid = s_axil.ARVALID;
assign s_axil_araddr = s_axil.ARADDR;
assign s_axil_arprot = s_axil.ARPROT;
assign s_axil_rready = s_axil.RREADY;
assign s_axil.RVALID = s_axil_rvalid;
assign s_axil.RDATA = s_axil_rdata;
assign s_axil.RRESP = s_axil_rresp;
{% endif %}

87
tests/lib/cpuifs/base.py Normal file
View File

@@ -0,0 +1,87 @@
from typing import List, TYPE_CHECKING
import os
import inspect
import jinja2 as jj
from peakrdl.regblock.cpuif.base import CpuifBase
from ..sv_line_anchor import SVLineAnchor
if TYPE_CHECKING:
from peakrdl.regblock import RegblockExporter
from ..sim_testcase import SimTestCase
class CpuifTestMode:
cpuif_cls = None # type: CpuifBase
# Files required by the DUT
# Paths are relative to the class that assigns this
rtl_files = [] # type: List[str]
# Files required by the sim testbench
# Paths are relative to the class that assigns this
tb_files = [] # type: List[str]
# Path is relative to the class that assigns this
tb_template = ""
def _get_class_dir_of_variable(self, varname:str) -> str:
"""
Traverse up the MRO and find the first class that explicitly assigns
the variable of name varname. Returns the directory that contains the
class definition.
"""
for cls in inspect.getmro(self.__class__):
if varname in cls.__dict__:
class_dir = os.path.dirname(inspect.getfile(cls))
return class_dir
raise RuntimeError
def _get_file_paths(self, varname:str) -> List[str]:
class_dir = self._get_class_dir_of_variable(varname)
files = getattr(self, varname)
cwd = os.getcwd()
new_files = []
for file in files:
relpath = os.path.relpath(
os.path.join(class_dir, file),
cwd
)
new_files.append(relpath)
return new_files
def get_sim_files(self) -> List[str]:
files = self._get_file_paths("rtl_files") + self._get_file_paths("tb_files")
unique_files = []
[unique_files.append(f) for f in files if f not in unique_files]
return unique_files
def get_synth_files(self) -> List[str]:
return self._get_file_paths("rtl_files")
def get_tb_inst(self, tb_cls: 'SimTestCase', exporter: 'RegblockExporter') -> str:
class_dir = self._get_class_dir_of_variable("tb_template")
loader = jj.FileSystemLoader(class_dir)
jj_env = jj.Environment(
loader=loader,
undefined=jj.StrictUndefined,
extensions=[SVLineAnchor],
)
context = {
"cpuif": self,
"cls": tb_cls,
"exporter": exporter,
"type": type,
}
template = jj_env.get_template(self.tb_template)
return template.render(context)

View File

@@ -0,0 +1,11 @@
from ..base import CpuifTestMode
from peakrdl.regblock.cpuif.passthrough import PassthroughCpuif
class Passthrough(CpuifTestMode):
cpuif_cls = PassthroughCpuif
rtl_files = []
tb_files = [
"passthrough_driver.sv",
]
tb_template = "tb_inst.sv"

View File

@@ -0,0 +1,119 @@
interface passthrough_driver #(
parameter DATA_WIDTH = 32,
parameter ADDR_WIDTH = 32
)(
input wire clk,
input wire rst,
output logic m_cpuif_req,
output logic m_cpuif_req_is_wr,
output logic [ADDR_WIDTH-1:0] m_cpuif_addr,
output logic [DATA_WIDTH-1:0] m_cpuif_wr_data,
input wire m_cpuif_req_stall_wr,
input wire m_cpuif_req_stall_rd,
input wire m_cpuif_rd_ack,
input wire m_cpuif_rd_err,
input wire [DATA_WIDTH-1:0] m_cpuif_rd_data,
input wire m_cpuif_wr_ack,
input wire m_cpuif_wr_err
);
timeunit 1ps;
timeprecision 1ps;
default clocking cb @(posedge clk);
default input #1step output #1;
output m_cpuif_req;
output m_cpuif_req_is_wr;
output m_cpuif_addr;
output m_cpuif_wr_data;
input m_cpuif_req_stall_wr;
input m_cpuif_req_stall_rd;
input m_cpuif_rd_ack;
input m_cpuif_rd_err;
input m_cpuif_rd_data;
input m_cpuif_wr_ack;
input m_cpuif_wr_err;
endclocking
task automatic reset();
cb.m_cpuif_req <= '0;
cb.m_cpuif_req_is_wr <= '0;
cb.m_cpuif_addr <= '0;
cb.m_cpuif_wr_data <= '0;
endtask
semaphore txn_req_mutex = new(1);
semaphore txn_resp_mutex = new(1);
task automatic write(logic [ADDR_WIDTH-1:0] addr, logic [DATA_WIDTH-1:0] data);
fork
begin
// Initiate transfer
txn_req_mutex.get();
##0;
cb.m_cpuif_req <= '1;
cb.m_cpuif_req_is_wr <= '1;
cb.m_cpuif_addr <= addr;
cb.m_cpuif_wr_data <= data;
@(cb);
while(cb.m_cpuif_req_stall_wr !== 1'b0) @(cb);
reset();
txn_req_mutex.put();
end
begin
// Wait for response
txn_resp_mutex.get();
@cb;
while(cb.m_cpuif_wr_ack !== 1'b1) @(cb);
txn_resp_mutex.put();
end
join
endtask
task automatic read(logic [ADDR_WIDTH-1:0] addr, output logic [DATA_WIDTH-1:0] data);
fork
begin
// Initiate transfer
txn_req_mutex.get();
##0;
cb.m_cpuif_req <= '1;
cb.m_cpuif_req_is_wr <= '0;
cb.m_cpuif_addr <= addr;
@(cb);
while(cb.m_cpuif_req_stall_rd !== 1'b0) @(cb);
reset();
txn_req_mutex.put();
end
begin
// Wait for response
txn_resp_mutex.get();
@cb;
while(cb.m_cpuif_rd_ack !== 1'b1) @(cb);
assert(!$isunknown(cb.m_cpuif_rd_data)) else $error("Read from 0x%0x returned X's on m_cpuif_rd_data", addr);
data = cb.m_cpuif_rd_data;
txn_resp_mutex.put();
end
join
endtask
task automatic assert_read(logic [ADDR_WIDTH-1:0] addr, logic [DATA_WIDTH-1:0] expected_data, logic [DATA_WIDTH-1:0] mask = '1);
logic [DATA_WIDTH-1:0] data;
read(addr, data);
data &= mask;
assert(data == expected_data) else $error("Read from 0x%x returned 0x%x. Expected 0x%x", addr, data, expected_data);
endtask
initial begin
reset();
end
initial forever begin
@cb;
if(!rst) assert(!$isunknown(cb.m_cpuif_rd_ack)) else $error("Saw X on m_cpuif_rd_ack!");
if(!rst) assert(!$isunknown(cb.m_cpuif_wr_ack)) else $error("Saw X on m_cpuif_wr_ack!");
end
endinterface

View File

@@ -0,0 +1,30 @@
{% sv_line_anchor %}
wire s_cpuif_req;
wire s_cpuif_req_is_wr;
wire [{{exporter.cpuif.addr_width-1}}:0] s_cpuif_addr;
wire [{{exporter.cpuif.data_width-1}}:0] s_cpuif_wr_data;
wire s_cpuif_req_stall_wr;
wire s_cpuif_req_stall_rd;
wire s_cpuif_rd_ack;
wire s_cpuif_rd_err;
wire [{{exporter.cpuif.data_width-1}}:0] s_cpuif_rd_data;
wire s_cpuif_wr_ack;
wire s_cpuif_wr_err;
passthrough_driver #(
.DATA_WIDTH({{exporter.cpuif.data_width}}),
.ADDR_WIDTH({{exporter.cpuif.addr_width}})
) cpuif (
.clk(clk),
.rst(rst),
.m_cpuif_req(s_cpuif_req),
.m_cpuif_req_is_wr(s_cpuif_req_is_wr),
.m_cpuif_addr(s_cpuif_addr),
.m_cpuif_wr_data(s_cpuif_wr_data),
.m_cpuif_req_stall_wr(s_cpuif_req_stall_wr),
.m_cpuif_req_stall_rd(s_cpuif_req_stall_rd),
.m_cpuif_rd_ack(s_cpuif_rd_ack),
.m_cpuif_rd_err(s_cpuif_rd_err),
.m_cpuif_rd_data(s_cpuif_rd_data),
.m_cpuif_wr_ack(s_cpuif_wr_ack),
.m_cpuif_wr_err(s_cpuif_wr_err)
);

74
tests/lib/sim_testcase.py Normal file
View File

@@ -0,0 +1,74 @@
from typing import List
import os
import jinja2 as jj
from .sv_line_anchor import SVLineAnchor
from .simulators.questa import Questa
from .simulators import StubSimulator
from .base_testcase import BaseTestCase
SIM_CLS = Questa
if os.environ.get("STUB_SIMULATOR", False):
SIM_CLS = StubSimulator
class SimTestCase(BaseTestCase):
#: Abort test if it exceeds this number of clock cycles
timeout_clk_cycles = 5000
simulator_cls = SIM_CLS
@classmethod
def _generate_tb(cls):
"""
Render the testbench template into actual tb.sv
"""
template_root_path = os.path.join(os.path.dirname(__file__), "..")
loader = jj.FileSystemLoader(
template_root_path
)
jj_env = jj.Environment(
loader=loader,
undefined=jj.StrictUndefined,
extensions=[SVLineAnchor],
)
context = {
"cls": cls,
"exporter": cls.exporter,
}
# template path needs to be relative to the Jinja loader root
template_path = os.path.join(cls.get_testcase_dir(), "tb_template.sv")
template_path = os.path.relpath(template_path, template_root_path)
template = jj_env.get_template(template_path)
output_path = os.path.join(cls.get_run_dir(), "tb.sv")
stream = template.stream(context)
stream.dump(output_path)
@classmethod
def setUpClass(cls):
super().setUpClass()
# Create testbench from template
cls._generate_tb()
simulator = cls.simulator_cls(testcase_cls=cls)
# cd into the build directory
cwd = os.getcwd()
os.chdir(cls.get_run_dir())
try:
simulator.compile()
finally:
# cd back
os.chdir(cwd)
def run_test(self, plusargs:List[str] = None) -> None:
simulator = self.simulator_cls(testcase_cls_inst=self)
simulator.run(plusargs)

View File

@@ -0,0 +1,33 @@
from typing import Type, TYPE_CHECKING, List
if TYPE_CHECKING:
from ..sim_testcase import SimTestCase
class Simulator:
def __init__(self, testcase_cls: 'Type[SimTestCase]' = None, testcase_cls_inst: 'SimTestCase' = None) -> None:
self.testcase_cls = testcase_cls
self.testcase_cls_inst = testcase_cls_inst
@property
def tb_files(self) -> List[str]:
files = []
files.extend(self.testcase_cls.cpuif.get_sim_files())
files.append("regblock_pkg.sv")
files.append("regblock.sv")
files.append("tb.sv")
return files
def compile(self) -> None:
raise NotImplementedError
def run(self, plusargs:List[str] = None) -> None:
raise NotImplementedError
class StubSimulator(Simulator):
def compile(self) -> None:
pass
def run(self, plusargs:List[str] = None) -> None:
pass

View File

@@ -0,0 +1,64 @@
from typing import List
import subprocess
import os
from . import Simulator
class Questa(Simulator):
def compile(self) -> None:
cmd = [
"vlog", "-sv", "-quiet", "-l", "build.log",
"+incdir+%s" % os.path.join(os.path.dirname(__file__), ".."),
# Use strict LRM conformance
"-svinputport=net",
# all warnings are errors
"-warning", "error",
# Ignore noisy warning about vopt-time checking of always_comb/always_latch
"-suppress", "2583",
]
# Add source files
cmd.extend(self.tb_files)
# Run command!
subprocess.run(cmd, check=True)
def run(self, plusargs:List[str] = None) -> None:
plusargs = plusargs or []
test_name = self.testcase_cls_inst.request.node.name
# call vsim
cmd = [
"vsim", "-quiet",
"-voptargs=+acc",
"-msgmode", "both",
"-do", "set WildcardFilter [lsearch -not -all -inline $WildcardFilter Memory]",
"-do", "log -r /*;",
"-do", "run -all; exit;",
"-c",
"-l", "%s.log" % test_name,
"-wlf", "%s.wlf" % test_name,
"tb",
]
for plusarg in plusargs:
cmd.append("+" + plusarg)
subprocess.run(cmd, check=True)
self.assertSimLogPass("%s.log" % test_name)
def assertSimLogPass(self, path: str):
self.testcase_cls_inst.assertTrue(os.path.isfile(path))
with open(path, encoding="utf-8") as f:
for line in f:
if line.startswith("# ** Error"):
self.testcase_cls_inst.fail(line)
elif line.startswith("# ** Fatal"):
self.testcase_cls_inst.fail(line)

View File

@@ -0,0 +1,61 @@
from typing import List
import subprocess
import os
from . import Simulator
class Xilinx(Simulator):
"""
Don't bother using the Xilinx simulator... Its buggy and extraordinarily slow.
As observed in v2021.1, clocking block assignments do not seem to actually simulate
correctly - assignment statements get ignored or the values get mangled.
Keeping this here in case someday it works better...
"""
def compile(self) -> None:
cmd = [
"xvlog", "--sv",
"--include", os.path.join(os.path.dirname(__file__), ".."),
]
cmd.extend(self.tb_files)
subprocess.run(cmd, check=True)
cmd = [
"xelab",
"--timescale", "1ns/1ps",
"--debug", "all",
"tb",
]
subprocess.run(cmd, check=True)
def run(self, plusargs:List[str] = None) -> None:
plusargs = plusargs or []
test_name = self.testcase_cls_inst.request.node.name
# call vsim
cmd = [
"xsim",
"--R",
"--log", "%s.log" % test_name,
"tb",
]
for plusarg in plusargs:
cmd.append("--testplusarg")
cmd.append(plusarg)
subprocess.run(cmd, check=True)
self.assertSimLogPass("%s.log" % test_name)
def assertSimLogPass(self, path: str):
self.testcase_cls_inst.assertTrue(os.path.isfile(path))
with open(path, encoding="utf-8") as f:
for line in f:
if line.startswith("Error:"):
self.testcase_cls_inst.fail(line)
elif line.startswith("Fatal:"):
self.testcase_cls_inst.fail(line)

View File

@@ -0,0 +1,10 @@
from jinja2_simple_tags import StandaloneTag
class SVLineAnchor(StandaloneTag):
"""
Define a custom Jinja tag that emits a SystemVerilog `line directive so that
assertion messages can get properly back-annotated
"""
tags = {"sv_line_anchor"}
def render(self):
return f'`line {self.lineno + 1} "{self.template}" 0'

View File

@@ -0,0 +1,36 @@
from typing import List
import subprocess
import os
import pytest
from .base_testcase import BaseTestCase
@pytest.mark.skipif(os.environ.get("SKIP_SYNTH_TESTS", False), reason="user skipped")
class SynthTestCase(BaseTestCase):
def _get_synth_files(self) -> List[str]:
files = []
files.extend(self.cpuif.get_synth_files())
files.append("regblock_pkg.sv")
files.append("regblock.sv")
return files
def run_synth(self) -> None:
script = os.path.join(
os.path.dirname(__file__),
"synthesis/vivado/run.tcl"
)
cmd = [
"vivado", "-nojournal", "-notrace",
"-mode", "batch",
"-log", "out.log",
"-source", script,
"-tclargs"
]
cmd.extend(self._get_synth_files())
subprocess.run(cmd, check=True)

View File

@@ -0,0 +1,8 @@
create_clock -period 10.000 -name clk [get_ports clk]
set_input_delay -clock [get_clocks clk] -min -add_delay 0.000 [get_ports -filter {(DIRECTION == IN) && (NAME != clk)}]
set_input_delay -clock [get_clocks clk] -max -add_delay 0.000 [get_ports -filter {(DIRECTION == IN) && (NAME != clk)}]
set_output_delay -clock [get_clocks clk] -min -add_delay 0.000 [get_ports -filter {DIRECTION == OUT}]
set_output_delay -clock [get_clocks clk] -max -add_delay 0.000 [get_ports -filter {DIRECTION == OUT}]

View File

@@ -0,0 +1,34 @@
set this_dir [file dirname [file normalize [info script]]]
set files $argv
# Multi-driven
set_msg_config -id {[Synth 8-6858]} -new_severity "ERROR"
set_msg_config -id {[Synth 8-6859]} -new_severity "ERROR"
# Implicit net
set_msg_config -id {[Synth 8-992]} -new_severity "ERROR"
# Non-combo always_comb
set_msg_config -id {[Synth 8-87]} -new_severity "ERROR"
# Latch
set_msg_config -id {[Synth 8-327]} -new_severity "ERROR"
# Timing loop
set_msg_config -id {[Synth 8-295]} -new_severity "ERROR"
# Promote all critical warnings to errors
set_msg_config -severity {CRITICAL WARNING} -new_severity "ERROR"
set_part xczu7eg-ffvf1517-2-i
read_verilog -sv $files
read_xdc $this_dir/constr.xdc
synth_design -top regblock -mode out_of_context
#write_checkpoint -force synth.dcp
if {[get_msg_config -count -severity {CRITICAL WARNING}] || [get_msg_config -count -severity ERROR]} {
error "Encountered errors"
}

99
tests/lib/tb_base.sv Normal file
View File

@@ -0,0 +1,99 @@
{% sv_line_anchor %}
module tb;
timeunit 1ns;
timeprecision 1ps;
logic rst = '1;
logic clk = '0;
initial forever begin
#5ns;
clk = ~clk;
end
//--------------------------------------------------------------------------
// DUT Signal declarations
//--------------------------------------------------------------------------
{%- if exporter.hwif.has_input_struct %}
regblock_pkg::regblock__in_t hwif_in;
{%- endif %}
{%- if exporter.hwif.has_output_struct %}
regblock_pkg::regblock__out_t hwif_out;
{%- endif %}
{%- block declarations %}
{%- endblock %}
//--------------------------------------------------------------------------
// Clocking
//--------------------------------------------------------------------------
default clocking cb @(posedge clk);
default input #1step output #1;
output rst;
{%- if exporter.hwif.has_input_struct %}
output hwif_in;
{%- endif %}
{%- if exporter.hwif.has_output_struct %}
input hwif_out;
{%- endif %}
{%- filter indent %}
{%- block clocking_dirs %}
{%- endblock %}
{%- endfilter %}
endclocking
//--------------------------------------------------------------------------
// CPUIF
//--------------------------------------------------------------------------
{{cls.cpuif.get_tb_inst(cls, exporter)|indent}}
//--------------------------------------------------------------------------
// DUT
//--------------------------------------------------------------------------
{% sv_line_anchor %}
regblock dut (.*);
{%- if exporter.hwif.has_output_struct %}
{% sv_line_anchor %}
initial begin
logic [$bits(hwif_out)-1:0] tmp;
forever begin
##1;
tmp = {>>{hwif_out}}; // Workaround for Xilinx's xsim - assign to tmp variable
if(!rst) assert(!$isunknown(tmp)) else $error("hwif_out has X's!");
end
end
{%- endif %}
{% sv_line_anchor %}
//--------------------------------------------------------------------------
// Test Sequence
//--------------------------------------------------------------------------
initial begin
cb.rst <= '1;
{%- if exporter.hwif.has_input_struct %}
cb.hwif_in <= '{default: '0};
{%- endif %}
begin
{%- filter indent(8) %}
{%- block seq %}
{%- endblock %}
{%- endfilter %}
end
{% sv_line_anchor %}
##5;
$finish();
end
//--------------------------------------------------------------------------
// Monitor for timeout
//--------------------------------------------------------------------------
initial begin
##{{cls.timeout_clk_cycles}};
$fatal(1, "Test timed out after {{cls.timeout_clk_cycles}} clock cycles");
end
endmodule

29
tests/lib/test_params.py Normal file
View File

@@ -0,0 +1,29 @@
from itertools import product
from .cpuifs.apb3 import APB3, FlatAPB3
from .cpuifs.axi4lite import AXI4Lite, FlatAXI4Lite
from .cpuifs.passthrough import Passthrough
all_cpuif = [
APB3(),
FlatAPB3(),
AXI4Lite(),
FlatAXI4Lite(),
Passthrough(),
]
def get_permutations(spec):
param_list = []
for v in product(*spec.values()):
param_list.append(dict(zip(spec, v)))
return param_list
#-------------------------------------------------------------------------------
# TODO: this wont scale well. Create groups of permutations. not necessary to permute everything all the time.
TEST_PARAMS = get_permutations({
"cpuif": all_cpuif,
"retime_read_fanin": [True, False],
"retime_read_response": [True, False],
"reuse_hwif_typedefs": [True, False],
})