testcase framework

This commit is contained in:
Alex Mykyta
2021-11-21 19:00:47 -08:00
parent d3c876a491
commit f70bdf774c
69 changed files with 1730 additions and 403 deletions

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

View File

View File

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

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,109 @@
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 reset();
cb.PSEL <= '0;
cb.PENABLE <= '0;
cb.PWRITE <= '0;
cb.PADDR <= '0;
cb.PWDATA <= '0;
endtask
task write(logic [ADDR_WIDTH-1:0] addr, logic [DATA_WIDTH-1:0] data);
##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();
endtask
task read(logic [ADDR_WIDTH-1:0] addr, output logic [DATA_WIDTH-1:0] data);
##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();
endtask
task assert_read(logic [ADDR_WIDTH-1:0] addr, logic [DATA_WIDTH-1:0] expected_data);
logic [DATA_WIDTH-1:0] data;
read(addr, data);
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,30 @@
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") %}
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 %}

50
test/lib/cpuifs/base.py Normal file
View File

@@ -0,0 +1,50 @@
from typing import List, TYPE_CHECKING
import os
import inspect
import jinja2 as jj
from peakrdl.regblock.cpuif.base import CpuifBase
if TYPE_CHECKING:
from peakrdl.regblock import RegblockExporter
from ..regblock_testcase import RegblockTestCase
class CpuifTestMode:
cpuif_cls = None # type: CpuifBase
tb_files = [] # type: List[str]
tb_template = ""
def get_tb_files(self) -> List[str]:
class_dir = os.path.dirname(inspect.getfile(self.__class__))
cwd = os.getcwd()
tb_files = []
for file in self.tb_files:
relpath = os.path.relpath(
os.path.join(class_dir, file),
cwd
)
tb_files.append(relpath)
return tb_files
def get_tb_inst(self, tb_cls: 'RegblockTestCase', exporter: 'RegblockExporter') -> str:
class_dir = os.path.dirname(inspect.getfile(self.__class__))
loader = jj.FileSystemLoader(
os.path.join(class_dir)
)
jj_env = jj.Environment(
loader=loader,
undefined=jj.StrictUndefined,
)
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,232 @@
from typing import Optional, List
import unittest
import os
import glob
import shutil
import subprocess
import inspect
import pytest
import jinja2 as jj
from systemrdl import RDLCompiler
from peakrdl.regblock import RegblockExporter
from .cpuifs.base import CpuifTestMode
from .cpuifs.apb3 import APB3
class RegblockTestCase(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
#: Abort test if it exceeds this number of clock cycles
timeout_clk_cycles = 1000
#: this gets auto-loaded via the _load_request autouse fixture
request = None # type: pytest.FixtureRequest
@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_build_dir(cls) -> str:
this_dir = cls.get_testcase_dir()
build_dir = os.path.join(this_dir, cls.__name__ + ".out")
return build_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_build_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) -> RegblockExporter:
"""
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)
exporter = RegblockExporter()
exporter.export(
root,
cls.get_build_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,
)
return exporter
@classmethod
def _generate_tb(cls, exporter: RegblockExporter):
"""
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,
)
context = {
"cls": cls,
"exporter": exporter,
}
# template path needs to be relative to the Jinja loader root
template_path = os.path.join(cls.get_testcase_dir(), "tb.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_build_dir(), "tb.sv")
stream = template.stream(context)
stream.dump(output_path)
@classmethod
def _compile_tb(cls):
# CD into the build directory
cwd = os.getcwd()
os.chdir(cls.get_build_dir())
cmd = [
"vlog", "-sv", "-quiet", "-l", "build.log",
# Free version of ModelSim throws errors if generate/endgenerate
# blocks are not used.
# These have been made optional long ago. Modern versions of SystemVerilog do
# not require them and I prefer not to add them.
"-suppress", "2720",
# Ignore noisy warning about vopt-time checking of always_comb/always_latch
"-suppress", "2583",
]
# Add CPUIF sources
cmd.extend(cls.cpuif.get_tb_files())
# Add DUT sources
cmd.append("regblock_pkg.sv")
cmd.append("regblock.sv")
# Add TB
cmd.append("tb.sv")
# Run command!
try:
subprocess.run(cmd, check=True)
finally:
# cd back
os.chdir(cwd)
@classmethod
def setUpClass(cls):
# Create fresh build dir
build_dir = cls.get_build_dir()
if os.path.exists(build_dir):
shutil.rmtree(build_dir)
os.mkdir(build_dir)
cls._write_params()
# Convert testcase RDL file --> SV
exporter = cls._export_regblock()
# Create testbench from template
cls._generate_tb(exporter)
cls._compile_tb()
def setUp(self) -> None:
# cd into the build directory
self.original_cwd = os.getcwd()
os.chdir(self.get_build_dir())
def run_test(self, plusargs:List[str] = None) -> None:
plusargs = plusargs or []
test_name = self.request.node.name
# call vsim
cmd = [
"vsim", "-quiet",
"-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 tearDown(self) -> None:
# cd back
os.chdir(self.original_cwd)
def assertSimLogPass(self, path: str):
self.assertTrue(os.path.isfile(path))
with open(path, encoding="utf-8") as f:
for line in f:
if line.startswith("# ** Error"):
self.fail(line)
elif line.startswith("# ** Fatal"):
self.fail(line)

View File

@@ -0,0 +1,90 @@
module tb;
timeunit 1ns;
timeprecision 1ps;
logic rst = '1;
logic clk = '0;
initial forever begin
#10ns;
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
//--------------------------------------------------------------------------
regblock dut (.*);
{%- if exporter.hwif.has_output_struct %}
initial forever begin
##1; if(!rst) assert(!$isunknown({>>{hwif_out}})) else $error("hwif_out has X's!");
end
{%- endif %}
//--------------------------------------------------------------------------
// 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
##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

23
test/lib/test_params.py Normal file
View File

@@ -0,0 +1,23 @@
from itertools import product
from .cpuifs.apb3 import APB3, FlatAPB3
all_cpuif = [
APB3(),
FlatAPB3(),
]
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 permutatuions. 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],
})