[fpvgen] FPV boilerplate generator script
This adds a boilerplate generator script for FPV testbenches. The
generator can be used for both comportable or noncomportable IPs.
Signed-off-by: Michael Schaffner <msf@opentitan.org>
diff --git a/util/_index.md b/util/_index.md
index a42b1f5..ebace13 100644
--- a/util/_index.md
+++ b/util/_index.md
@@ -15,4 +15,5 @@
* `testplanner.py` utility for generating verification testplans from Hjson descriptors [README]({{< relref "util/testplanner/README.md" >}})
* `tlgen.py` utility for TileLink bus matrix [README]({{< relref "util/tlgen/README.md" >}})
* `uvmdvgen.py` utility for generating testbenches for comportable IP [README]({{< relref "util/uvmdvgen/README.md" >}})
+* `fpvgen.py` utility for generating FPV testbenches for comportable IP [README]({{< relref "util/fpvgen/README.md" >}})
* `wavegen.py` utility for creating technical waveforms in documentation [README]({{< relref "util/wavegen/README.md" >}})
diff --git a/util/fpvgen.py b/util/fpvgen.py
new file mode 100755
index 0000000..91eb061
--- /dev/null
+++ b/util/fpvgen.py
@@ -0,0 +1,123 @@
+#!/usr/bin/env python3
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+r"""Generates boilerplate code for the FPV testbench setup.
+"""
+import argparse
+import sys
+from io import StringIO
+from pathlib import Path
+
+from mako.template import Template
+
+import fpvgen.sv_parse as sv_parse
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ prog="fpvgen",
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ description="""\
+ Boilerplate code generator for FPV testbenches. Can be used for
+ comportable or non-comportable IPs.
+
+ The generator creates the FuseSoC core file and two additional
+ subfolders 'tb' and 'vip' in the output directory. It will place stubs
+ for the testbench and bind files into the 'tb' subfolder, and a stub for
+ the FPV assertions into the 'vip' (verification IP) subfolder.
+
+ The generator needs the path to the top-level module of the IP to be
+ tested. E.g., suppose we would like to generate an FPV testbench for a
+ FIFO primitive located at 'hw/ip/prim/rtl/prim_fifo_sync.sv' we can
+ invoke the generator as follows:
+
+ util/fpvgen.py hw/ip/prim/rtl/prim_fifo_sync.sv
+
+ By default, the output directory is assumed to be '../fpv' with respect
+ to the toplevel module, but this can be overriden using the -eo switch.
+
+ Further if the IP is comportable, this can be indicated using the -c
+ switch, which causes the generator to add a bind statement for the CSR
+ FPV assertions in the testbench.""",
+ add_help=True)
+ parser.add_argument(
+ 'file',
+ type=str,
+ help="""Relative path to the SystemVerilog file of the module for which
+ the code shall be generated. This can be a primitive or a comportable IP
+ (for which the -c switch should be set)."""
+ )
+
+ parser.add_argument(
+ '-o',
+ '--outdir',
+ type=str,
+ default="",
+ help="""Path where to place the testbench code. This is defaults to
+ '../fpv' w.r.t. to the module path. For instance, if the module path is
+ 'hw/ip/mymod/rtl/mymod.sv', the FPV testbench would be generated under
+ hw/ip/mymod/fpv. """
+ )
+ parser.add_argument(
+ '-c',
+ '--is_cip',
+ action="store_true",
+ default=False,
+ help="""Indicates whether this is a comportable IP. If yes, FPV
+ assertions for the TL-UL interface and CSRs are automatically bound in
+ the testbench. Note however that these CSR assertions need to be
+ generated separately using the regtool automation."""
+ )
+
+ args = parser.parse_args()
+
+ mod_path = Path(args.file)
+ if not mod_path.is_file() or mod_path.suffix != ".sv":
+ print("Error: %s is not a module or does not exist" % str(mod_path))
+ return 1
+
+ if not args.outdir:
+ # the default output path is ../fpv with
+ # respect to the module location
+ parentpath = mod_path.absolute().parent.parent
+ outpath = parentpath.joinpath("fpv")
+ else:
+ outpath = args.outdir
+
+ print("Output path is: %s" % outpath)
+
+ dut = sv_parse.parse_file(mod_path)
+ dut.is_cip = args.is_cip
+
+ # always include the prims
+ dut.deps += ["lowrisc:prim:all"]
+
+ if args.is_cip:
+ # for TL-UL assertions
+ dut.deps += ["lowrisc:ip:tlul"]
+ # in this case the parent directory is
+ # likely the correct basename of the IP
+ dut.deps += ["lowrisc:ip:" + parentpath.stem]
+
+ # define template files to iterate over
+ template_files = [(Path(__file__).parent.joinpath("fpvgen/fpv.sv.tpl"), \
+ outpath.joinpath("tb").joinpath(mod_path.stem + "_fpv.sv")), \
+ (Path(__file__).parent.joinpath("fpvgen/bind_fpv.sv.tpl"), \
+ outpath.joinpath("tb").joinpath(mod_path.stem + "_bind_fpv.sv")), \
+ (Path(__file__).parent.joinpath("fpvgen/assert_fpv.sv.tpl"), \
+ outpath.joinpath("vip").joinpath(mod_path.stem + "_assert_fpv.sv")), \
+ (Path(__file__).parent.joinpath("fpvgen/fusesoc.core.tpl"), \
+ outpath.joinpath(mod_path.stem + "_fpv.core"))]
+
+ for (tpl_file, out_file) in template_files:
+ print("Generating %s" % str(out_file))
+ out_file.parent.mkdir(parents=True, exist_ok=True)
+ tpl = Template(tpl_file.read_text())
+ out_file.write_text(tpl.render(dut=dut))
+
+ return 0
+
+
+if __name__ == "__main__":
+ main()
diff --git a/util/fpvgen/README.md b/util/fpvgen/README.md
new file mode 100644
index 0000000..550f9fc
--- /dev/null
+++ b/util/fpvgen/README.md
@@ -0,0 +1,86 @@
+---
+title: "Fpvgen: Initial FPV testbench generation tool"
+---
+
+# Overview
+
+`fpvgen` is a Python tool that can be used to generate the initial boilerplate code for an FPV testbench.
+It takes as input a SystemVerilog module file representing the top-level of an IP to be tested with FPV, and generates the following folders and files in the output directory (which defaults to `../fpv` with respect to the module file provided):
+```console
+.
+├── fpv // default output folder
+│ ├── <ip-name>_fpv.core
+│ ├── tb
+│ │ ├── <ip-name>_bind_fpv.sv
+│ │ └── <ip-name>_fpv.sv
+│ └── vip
+│ └── <ip-name>_assert_fpv.sv
+└── rtl // folder containing the SV module file
+ ├── <ip-name>.sv
+ ...
+```
+The `<ip-name>_fpv.sv` is the FPV testbench that can be used to instantiate different parameterizations of the DUT to be tested.
+`<ip-name>_bind_fpv.sv` contains the bind statement which binds the verification IP `<ip-name>_assert_fpv.sv` to all DUT instances.
+If the IP is flagged as being comportable using the `-c` switch, the CSR FPV assertions are bound to the module as well.
+
+# Examples
+Generating a non-comportable IP can be done as follows (using the LFSR as an example):
+```console
+util/fpvgen.py hw/ip/prim/rtl/prim_lfsr.sv
+```
+
+If the IP is comportable, only the `-c` switch has to be added.
+E.g., using the `pinmux` comportable IP as an example:
+```console
+util/fpvgen.py -c hw/ip/pinmux/rtl/pinmux.sv
+```
+
+If needed, the default output directory can be overridden using the `-o` switch.
+
+# Help Output
+This is the help output from the tool (switch `-h`).
+```console
+usage: fpvgen [-h] [-o OUTDIR] [-c] file
+
+ Boilerplate code generator for FPV testbenches. Can be used for
+ comportable or non-comportable IPs.
+
+ The generator creates the FuseSoC core file and two additional
+ subfolders 'tb' and 'vip' in the output directory. It will place stubs
+ for the testbench and bind files into the 'tb' subfolder, and a stub for
+ the FPV assertions into the 'vip' (verification IP) subfolder.
+
+ The generator needs the path to the top-level module of the IP to be
+ tested. E.g., suppose we would like to generate an FPV testbench for a
+ FIFO primitive located at 'hw/ip/prim/rtl/prim_fifo_sync.sv' we can
+ invoke the generator as follows:
+
+ util/fpvgen.py hw/ip/prim/rtl/prim_fifo_sync.sv
+
+ By default, the output directory is assumed to be '../fpv' with respect
+ to the toplevel module, but this can be overriden using the -eo switch.
+
+ Further if the IP is comportable, this can be indicated using the -c
+ switch, which causes the generator to add a bind statement for the CSR
+ FPV assertions in the testbench.
+
+positional arguments:
+ file Relative path to the SystemVerilog file of the module
+ for which the code shall be generated. This can be a
+ primitive or a comportable IP (for which the -c switch
+ should be set).
+
+optional arguments:
+ -h, --help show this help message and exit
+ -o OUTDIR, --outdir OUTDIR
+ Path where to place the testbench code. This is
+ defaults to '../fpv' w.r.t. to the module path. For
+ instance, if the module path is
+ 'hw/ip/mymod/rtl/mymod.sv', the FPV testbench would be
+ generated under hw/ip/mymod/fpv.
+ -c, --is_cip Indicates whether this is a comportable IP. If yes,
+ FPV assertions for the TL-UL interface and CSRs are
+ automatically bound in the testbench. Note however
+ that these CSR assertions need to be generated
+ separately using the regtool automation.
+```
diff --git a/util/fpvgen/__init__.py b/util/fpvgen/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/util/fpvgen/__init__.py
diff --git a/util/fpvgen/assert_fpv.sv.tpl b/util/fpvgen/assert_fpv.sv.tpl
new file mode 100644
index 0000000..108e8f5
--- /dev/null
+++ b/util/fpvgen/assert_fpv.sv.tpl
@@ -0,0 +1,52 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+//
+// Assertions for ${dut.name}.
+// Intended to be used with a formal tool.
+
+% if len(dut.pkgs) > 0:
+module ${dut.name}_fpv
+% for pkg in dut.pkgs:
+ import ${pkg};
+% endfor
+% if dut.params:
+#(
+% else:
+(
+% endif
+% else:
+% if dut.params:
+module ${dut.name}_assert_fpv #(
+% else:
+module ${dut.name}_assert_fpv (
+% endif
+% endif
+% if dut.params:
+% for k, param in enumerate(dut.params):
+<% comma = "" if (k == len(dut.params)-1) else "," %> ${param.style} ${param.datatype} ${param.name} =${param.value}${comma}
+% endfor
+) (
+% endif
+% for k, port in enumerate(dut.ports):
+<% comma = "" if (k == len(dut.ports)-1) else "," %> input ${port.datatype} ${port.name}${comma}
+% endfor
+);
+
+ ///////////////////////////////
+ // Declarations & Parameters //
+ ///////////////////////////////
+
+ /////////////////
+ // Assumptions //
+ /////////////////
+
+ // `ASSUME(MyAssumption_M, ..., clk_i, !rst_ni)
+
+ ////////////////
+ // Assertions //
+ ////////////////
+
+ // `ASSUME(MyAssertions_A, ..., clk_i, !rst_ni)
+
+endmodule : ${dut.name}_assert_fpv
diff --git a/util/fpvgen/bind_fpv.sv.tpl b/util/fpvgen/bind_fpv.sv.tpl
new file mode 100644
index 0000000..e2a5af5
--- /dev/null
+++ b/util/fpvgen/bind_fpv.sv.tpl
@@ -0,0 +1,27 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+//
+
+module ${dut.name}_bind_fpv;
+
+ bind ${dut.name} ${dut.name}_assert_fpv ${dut.name}_assert_fpv (
+ .*
+ );
+% if dut.is_cip:
+
+ bind ${dut.name} tlul_assert #(
+ .EndpointType("Device")
+ ) tlul_assert_device (
+ .clk_i,
+ .rst_ni,
+ .h2d (tl_i),
+ .d2h (tl_o)
+ );
+
+ bind ${dut.name} ${dut.name}_csr_assert_fpv (
+ .*
+ );
+% endif
+
+endmodule : ${dut.name}_bind_fpv
diff --git a/util/fpvgen/fpv.sv.tpl b/util/fpvgen/fpv.sv.tpl
new file mode 100644
index 0000000..c3d6d27
--- /dev/null
+++ b/util/fpvgen/fpv.sv.tpl
@@ -0,0 +1,51 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+//
+// Testbench module for ${dut.name}.
+// Intended to be used with a formal tool.
+
+% if len(dut.pkgs) > 0:
+module ${dut.name}_fpv
+% for pkg in dut.pkgs:
+ import ${pkg};
+% endfor
+% if dut.params:
+#(
+% else:
+(
+% endif
+% else:
+% if dut.params:
+module ${dut.name}_fpv #(
+% else:
+module ${dut.name}_fpv (
+% endif
+% endif
+% if dut.params:
+% for k, param in enumerate(dut.params):
+<% comma = "" if (k == len(dut.params)-1) else "," %> ${param.style} ${param.datatype} ${param.name} =${param.value}${comma}
+% endfor
+) (
+% endif
+% for k, port in enumerate(dut.ports):
+<% comma = "" if (k == len(dut.ports)-1) else "," %> ${port.direction} ${port.datatype} ${port.name}${comma}
+% endfor
+);
+
+% if dut.params:
+ ${dut.name} #(
+% for k, param in enumerate(dut.params):
+ <% comma = "" if (k == len(dut.params)-1) else "," %> .${param.name}(${param.name})${comma}
+% endfor
+ ) i_${dut.name} (
+% else:
+ ${dut.name} i_${dut.name} (
+%endif
+ % for k, port in enumerate(dut.ports):
+<% comma = "" if (k == len(dut.ports)-1) else "," %> .${port.name}${comma}
+ % endfor
+ );
+
+
+endmodule : ${dut.name}_fpv
diff --git a/util/fpvgen/fusesoc.core.tpl b/util/fpvgen/fusesoc.core.tpl
new file mode 100644
index 0000000..8db8a75
--- /dev/null
+++ b/util/fpvgen/fusesoc.core.tpl
@@ -0,0 +1,29 @@
+CAPI=2:
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+name: "lowrisc:fpv:${dut.name}_fpv:0.1"
+description: "${dut.name} FPV target"
+filesets:
+ files_fpv:
+ depend:
+% for dep in dut.deps:
+ - ${dep}
+% endfor
+ files:
+ - vip/${dut.name}_assert_fpv.sv
+ - tb/${dut.name}_bind_fpv.sv
+ - tb/${dut.name}_fpv.sv
+% if dut.is_cip:
+ - vip/${dut.name}_csr_assert_fpv.sv
+% endif
+ file_type: systemVerilogSource
+
+targets:
+ default:
+ # note, this setting is just used
+ # to generate a file list for jg
+ default_tool: icarus
+ filesets:
+ - files_fpv
+ toplevel: ${dut.name}_fpv
diff --git a/util/fpvgen/sv_parse.py b/util/fpvgen/sv_parse.py
new file mode 100644
index 0000000..325e798
--- /dev/null
+++ b/util/fpvgen/sv_parse.py
@@ -0,0 +1,231 @@
+#!/usr/bin/env python3
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+r"""Helper functions for parsing a systemverilog module header.
+"""
+
+from io import StringIO
+from pathlib import Path
+
+
+class Param():
+ name = ""
+ datatype = ""
+ style = ""
+ value = ""
+
+ def __init__(self, name_="", datatype_="", style_="", value_=""):
+ self.name = name_
+ self.datatype = datatype_
+ self.style = style_
+ self.value = value_
+
+
+class Port():
+ name = ""
+ datatype = ""
+ direction = ""
+
+ def __init__(self, name_="", datatype_="", direction_=""):
+ self.name = name_
+ self.datatype = datatype_
+ self.direction = direction_
+
+
+class Dut():
+ name = ""
+ pkgs = []
+ params = []
+ ports = []
+ deps = []
+ is_cip = False
+
+ def __init__(self, name_="", pkgs_=[], ports_=[], \
+ params_=[], deps_=[], is_cip_=False):
+ self.name = name_
+ self.pkgs = pkgs_
+ self.ports = ports_
+ self.params = params_
+ self.deps = deps_
+ self.is_cip = is_cip_
+
+
+# strip // comments
+def strip_comments(buf):
+ outbuf = ""
+ for line in buf.split('\n'):
+ for k in range(len(line) - 1):
+ if line[k:k + 2] == "//":
+ break
+ else:
+ outbuf += line[k]
+ else:
+ if line:
+ outbuf += line[-1]
+ outbuf += " "
+
+ return outbuf
+
+
+PARENTH_STYLES = {'(': ')', '[': ']', '{': '}'}
+
+
+# parse parenthesis and optionally handle the body using the handler function
+# if no handler is specified, it just calls itself recursively
+def parse_parenthesis(hdl_raw, dut, style='(', handler=None):
+ if style not in PARENTH_STYLES:
+ print("Unknown parenthesis style %s, aborting." % style)
+ else:
+ par_opened = False
+ while hdl_raw:
+ c = hdl_raw.pop(0)
+ if c == style:
+ par_opened = True
+ if handler:
+ handler(hdl_raw, dut)
+ else:
+ parse_parenthesis(hdl_raw, dut, style)
+ if c == PARENTH_STYLES[style]:
+ if not par_opened:
+ hdl_raw.insert(0, c)
+ break
+ return
+
+
+# parse individual port declarations
+# may not be fully correct with unpacked dimensions,
+# but works for the common case
+def parse_port(buf, dut):
+ words = buf.split()
+ if words:
+ if words[0] not in ["input", "inout", "output"]:
+ print("Warning, expected input, inout or output keyword")
+ else:
+ if len(words) > 2:
+ dut.ports += [Port(words[-1], "".join(words[1:-1]), words[0])]
+ elif len(words) == 2:
+ dut.ports += [Port(words[-1], "", words[0])]
+ else:
+ print("Warning, port declaration incomplete")
+ else:
+ print("Warning, port declaration empty")
+ return
+
+
+# parse individual parameter declaration
+def parse_param(buf, dut):
+ words = buf.split('=')
+ value = '='.join(words[1:])
+ words = words[0].split()
+
+ if words:
+ if words[0] not in ["parameter", "localparam"]:
+ print("Warning, expected parameter or localparam keyword")
+ else:
+ if len(words) > 2:
+ dut.params += [
+ Param(words[-1], " ".join(words[1:-1]), words[0], value)
+ ]
+ elif len(words) == 2:
+ dut.params += [Param(words[-1], "", words[0], value)]
+ else:
+ print("Warning, parameter declaration incomplete")
+ else:
+ print("Warning, port declaration empty")
+ return
+
+
+# extract individual declarations
+def parse_declaration(hdl_raw, dut, handler):
+ buf = ""
+ par_opened = 0
+ while hdl_raw:
+ c = hdl_raw.pop(0)
+ # end of this port
+ if c == ',':
+ handler(buf, dut)
+ buf = ""
+ elif c == '(':
+ par_opened = par_opened + 1
+ buf += c
+ elif c == ')':
+ if par_opened:
+ # part of the declaration
+ par_opened = par_opened - 1
+ buf += c
+ else:
+ # end of the declaration list
+ handler(buf, dut)
+ hdl_raw.insert(0, ')')
+ break
+ else:
+ buf += c
+ return
+
+
+def parse_ports(hdl_raw, dut):
+ parse_declaration(hdl_raw, dut, parse_port)
+
+
+def parse_params(hdl_raw, dut):
+ parse_declaration(hdl_raw, dut, parse_param)
+
+
+def parse_module(words, dut):
+ # check for imports first
+ while words:
+ w = words.pop(0)
+ if w == "import":
+ if words:
+ # get package names to import
+ pkg = words.pop(0).split(";")
+ dut.pkgs += [pkg[0]]
+ else:
+ print("Unexpected end")
+ # stop package scan and move on to body
+ elif '#' in w or '(' in w:
+ words.insert(0, w)
+ break
+
+ hdl_raw = list(' '.join(words))
+ while hdl_raw:
+ c = hdl_raw.pop(0)
+ if c == '#':
+ parse_parenthesis(hdl_raw, dut, '(', parse_params)
+ elif c == '(':
+ hdl_raw.insert(0, '(')
+ parse_parenthesis(hdl_raw, dut, '(', parse_ports)
+ elif c == ';':
+ break
+ return
+
+
+# simplistic module declaration parser.
+# this works in most cases, but there are exceptions.
+def parse_file(file):
+ dut = Dut()
+ hdl_raw = ""
+ with open(file, 'r') as fp:
+ hdl_raw = strip_comments(fp.read())
+ # extract first module declaration in file and extract port list
+ # also look for imported packages (either in the module declaration
+ # or before it)
+ words = hdl_raw.split()
+ while words:
+ w = words.pop(0)
+ if w == "import":
+ if words:
+ # get package names to import
+ pkg = words.pop(0).split(";")
+ dut.pkgs += [pkg[0]]
+ else:
+ print("Unexpected end")
+ elif w == "module":
+ if words:
+ # get module name
+ dut.name = words[0]
+ # parse the module params and port list and exit
+ parse_module(words, dut)
+ break
+ return dut