blob: e746cb59cb0835aec5056963f1c468bfd4d2b004 [file] [log] [blame]
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Convenience wrapper for Verilator driven cocotb."""
load("@kelvin_hw//third_party/python:requirements.bzl", "requirement")
load("@rules_hdl//cocotb:cocotb.bzl", "cocotb_test")
load("@rules_python//python:defs.bzl", "py_library")
def _verilator_cocotb_model_impl(ctx):
"""Implementation of the verilator_cocotb_model rule."""
cc_toolchain = ctx.toolchains["@bazel_tools//tools/cpp:toolchain_type"].cc
ar_executable = cc_toolchain.ar_executable
compiler_executable = cc_toolchain.compiler_executable
ld_executable = cc_toolchain.ld_executable
hdl_toplevel = ctx.attr.hdl_toplevel
outdir_name = hdl_toplevel + "_build"
output_file = ctx.actions.declare_file(outdir_name + "/" + hdl_toplevel)
make_log = ctx.actions.declare_file(outdir_name + "/make.log")
outdir = output_file.dirname
verilator_root = "$PWD/{}.runfiles/kelvin_hw/external/verilator".format(ctx.executable._verilator_bin.path)
cocotb_lib_path = "$PWD/{}".format(ctx.files._cocotb_verilator_lib[0].dirname)
verilator_cmd = " ".join("""
VERILATOR_ROOT={verilator_root} {verilator} \
-cc \
--exe \
-Mdir {outdir} \
--top-module {hdl_toplevel} \
--vpi \
--public-flat-rw \
--prefix Vtop \
-o {hdl_toplevel} \
-LDFLAGS "-Wl,-rpath {cocotb_lib_path} -L{cocotb_lib_path} -lcocotbvpi_verilator" \
{trace} \
{cflags} \
$PWD/{verilator_cpp} \
{verilog_source}
""".strip().split("\n")).format(
verilator = ctx.executable._verilator_bin.path,
verilator_root = verilator_root,
outdir = outdir,
hdl_toplevel = hdl_toplevel,
cocotb_lib_path = cocotb_lib_path,
cflags = " ".join(ctx.attr.cflags),
verilator_cpp = ctx.files._cocotb_verilator_cpp[0].path,
verilog_source = ctx.file.verilog_source.path,
trace = "--trace" if ctx.attr.trace else "",
)
make_cmd = "PATH=`dirname {ld}`:$PATH make -j $(nproc) -C {outdir} -f Vtop.mk {trace} CXX={cxx} AR={ar} LINK={cxx} > {make_log} 2>&1".format(
outdir = outdir,
cocotb_lib_path = cocotb_lib_path,
make_log = make_log.path,
trace = "VM_TRACE=1" if ctx.attr.trace else "",
ar = ar_executable,
ld = ld_executable,
cxx = compiler_executable,
)
script = " && ".join([verilator_cmd.strip(), make_cmd])
ctx.actions.run_shell(
outputs = [output_file, make_log],
tools = ctx.files._verilator_bin,
inputs = depset(
transitive = [
depset(ctx.files._verilator),
depset(ctx.files._cocotb_verilator_lib),
depset(ctx.files._cocotb_verilator_cpp),
depset([ctx.file.verilog_source]),
],
),
command = script,
mnemonic = "Verilate",
)
return [
DefaultInfo(
files = depset([output_file, make_log]),
runfiles = ctx.runfiles(files = [output_file, make_log]),
executable = output_file,
),
OutputGroupInfo(
all_files = depset([output_file, make_log]),
),
]
verilator_cocotb_model = rule(
doc = """Builds a verilator model for cocotb.
This rule takes a verilog source file and a toplevel module name and
builds a verilator model that can be used with cocotb.
It returns a DefaultInfo provider with an executable that can be run
to execute the simulation.
Attributes:
verilog_source: The verilog source file to build the model from.
hdl_toplevel: The name of the toplevel module.
cflags: A list of flags to pass to the compiler.
""",
implementation = _verilator_cocotb_model_impl,
attrs = {
"verilog_source": attr.label(allow_single_file = True, mandatory = True),
"hdl_toplevel": attr.string(mandatory = True),
"cflags": attr.string_list(default = []),
"trace": attr.bool(default = False),
"_verilator": attr.label(
default = "@verilator//:verilator",
executable = True,
cfg = "exec",
),
"_verilator_bin": attr.label(
default = "@verilator//:verilator_bin",
executable = True,
cfg = "exec",
),
"_cocotb_verilator_lib": attr.label(
default = "@kelvin_pip_deps_cocotb//:verilator_libs",
allow_files = True,
),
"_cocotb_verilator_cpp": attr.label(
default = "@kelvin_pip_deps_cocotb//:verilator_srcs",
allow_files = True,
),
},
executable = True,
toolchains = ["@bazel_tools//tools/cpp:toolchain_type"],
)
def verilator_cocotb_test(
name,
model,
hdl_toplevel,
test_module,
deps = [],
data = [],
**kwargs):
"""Runs a cocotb test with a verilator model.
This is a wrapper around the cocotb_test rule that is specific to
verilator.
Args:
name: The name of the test.
model: The verilator_cocotb_model target to use.
hdl_toplevel: The name of the toplevel module.
test_module: The python module that contains the test.
deps: Additional dependencies for the test.
data: Data dependencies for the test.
**kwargs: Additional arguments to pass to the cocotb_test rule.
"""
kwargs.update(
hdl_toplevel_lang = "verilog",
sim_name = "verilator",
sim = [
"@verilator//:verilator",
"@verilator//:verilator_bin",
],
)
# Wrap in py_library so we can forward data
py_library(
name = name + "_test_data",
srcs = [],
deps = deps + [
requirement("cocotb"),
requirement("numpy"),
requirement("pytest"),
],
data = data,
)
cocotb_test(
name = name,
model = model,
hdl_toplevel = hdl_toplevel,
test_module = test_module,
deps = [
":{}_test_data".format(name),
],
**kwargs
)
def _verilator_cocotb_test_suite(
name,
model,
testcases = [],
testcases_vname = "",
tests_kwargs = {},
**kwargs):
"""Runs a cocotb test with a verilator model.
This is a wrapper around the cocotb_test rule that is specific to
verilator.
Args:
name: The name of the test.
model: The verilator_cocotb_model target to use.
testcases: A list of testcases to run. A test will be generated for each
testcase.
tests_kwargs: A dictionary of arguments to pass to the cocotb_test rule.
**kwargs: Additional arguments to pass to the cocotb_test rule.
"""
all_tests_kwargs = dict(tests_kwargs)
all_tests_kwargs.update(kwargs)
if testcases:
test_targets = []
for tc in testcases:
tc_tests_kwargs = dict(all_tests_kwargs)
tags = list(tc_tests_kwargs.pop("tags", []))
tags.append("manual")
tags.append("verilator_cocotb_single_test")
verilator_cocotb_test(
name = "{}_{}".format(name, tc),
model = model,
testcase = [tc],
tags = tags,
**tc_tests_kwargs
)
test_targets.append(":{}_{}".format(name, tc))
# Generate a meta-target for all tests.
meta_target_kwargs = dict(all_tests_kwargs)
tags = list(meta_target_kwargs.pop("tags", []))
tags.append("verilator_cocotb_test_suite")
if testcases_vname:
tags.append("testcases_vname={}".format(testcases_vname))
verilator_cocotb_test(
name = name,
model = model,
tags = tags,
**meta_target_kwargs
)
def vcs_cocotb_test(
name,
hdl_toplevel,
test_module,
deps = [],
data = [],
**kwargs):
"""Runs a cocotb test with a vcs model.
This is a wrapper around the cocotb_test rule that is specific to
vcs.
Args:
name: The name of the test.
hdl_toplevel: The name of the toplevel module.
test_module: The python module that contains the test.
deps: Additional dependencies for the test.
data: Data dependencies for the test.
**kwargs: Additional arguments to pass to the cocotb_test rule.
"""
tags = list(kwargs.pop("tags", []))
tags.append("vcs")
kwargs.update(
hdl_toplevel_lang = "verilog",
sim_name = "vcs",
sim = [],
tags = tags,
)
# Wrap in py_library so we can forward data
py_library(
name = name + "_test_data",
srcs = [],
deps = deps + [
requirement("cocotb"),
requirement("numpy"),
requirement("pytest"),
],
data = data,
)
cocotb_test(
name = name,
hdl_toplevel = hdl_toplevel,
test_module = test_module,
deps = [
":{}_test_data".format(name),
],
**kwargs
)
def _vcs_cocotb_test_suite(
name,
verilog_sources,
testcases = [],
testcases_vname = "",
tests_kwargs = {},
**kwargs):
"""Runs a cocotb test with a vcs model.
This is a wrapper around the cocotb_test rule that is specific to
vcs.
Args:
name: The name of the test.
verilog_sources: The verilog sources to use for the test.
testcases: A list of testcases to run. A test will be generated for each
testcase.
tests_kwargs: A dictionary of arguments to pass to the cocotb_test rule.
**kwargs: Additional arguments to pass to the cocotb_test rule.
"""
all_tests_kwargs = dict(tests_kwargs)
all_tests_kwargs.update(kwargs)
hdl_toplevel = all_tests_kwargs.get("hdl_toplevel")
if not hdl_toplevel:
fail("hdl_toplevel must be specified in tests_kwargs")
if testcases:
test_targets = []
for tc in testcases:
tc_tests_kwargs = dict(all_tests_kwargs)
tags = list(tc_tests_kwargs.pop("tags", []))
tags.append("manual")
tags.append("vcs_cocotb_single_test")
test_args = tc_tests_kwargs.pop("test_args", [""])
vcs_cocotb_test(
name = "{}_{}".format(name, tc),
testcase = [tc],
tags = tags,
test_args = ["{} -cm_name {}".format(test_args[0], tc)] + test_args[1:],
verilog_sources = verilog_sources,
**tc_tests_kwargs
)
test_targets.append(":{}_{}".format(name, tc))
# Generate a meta-target for all tests.
meta_target_kwargs = dict(all_tests_kwargs)
tags = list(meta_target_kwargs.pop("tags", []))
tags.append("vcs_cocotb_test_suite")
vcs_cocotb_test(
name = name,
tags = tags,
verilog_sources = verilog_sources,
**meta_target_kwargs
)
def cocotb_test_suite(name, testcases, simulators = ["verilator"], **kwargs):
"""Runs a cocotb test with a verilator or vcs model.
This is a wrapper around the cocotb_test rule that is specific to
verilator.
Args:
name: The name of the test.
simulators: A list of simulators to run the test with.
Supported simulators are "verilator" and "vcs".
**kwargs: Additional arguments to pass to the cocotb_test rule.
These can be prefixed with the simulator name to apply them to
only that simulator.
"""
# Pop tests_kwargs from kwargs, if it exists.
tests_kwargs = kwargs.pop("tests_kwargs", {})
testcases_vname = kwargs.pop("testcases_vname", "")
for sim in simulators:
sim_kwargs = {}
sim_tests_kwargs = dict(tests_kwargs)
# Partition kwargs into sim_kwargs
for key, value in kwargs.items():
if key.startswith(sim):
sim_kwargs[key.replace(sim + "_", "")] = value
# Partition tests_kwargs into sim_tests_kwargs
for key, value in tests_kwargs.items():
if key.startswith(sim):
sim_tests_kwargs[key.replace(sim + "_", "")] = value
# Remove sim-specific kwargs from tests_kwargs
for key, value in tests_kwargs.items():
if key.startswith(sim):
if key in sim_tests_kwargs:
sim_tests_kwargs.pop(key)
if sim == "verilator":
model = sim_kwargs.pop("model", None)
if not model:
fail("verilator_model must be specified for verilator tests")
_verilator_cocotb_test_suite(
name = name,
model = model,
testcases = testcases,
testcases_vname = testcases_vname,
tests_kwargs = sim_tests_kwargs,
**sim_kwargs
)
elif sim == "vcs":
verilog_sources = sim_kwargs.pop("verilog_sources", [])
if not verilog_sources:
fail("vcs_verilog_sources must be specified for vcs tests")
_vcs_cocotb_test_suite(
name = "{}_{}".format(sim, name),
verilog_sources = verilog_sources,
testcases = testcases,
testcases_vname = testcases_vname,
tests_kwargs = sim_tests_kwargs,
**sim_kwargs
)
else:
fail("Unknown simulator: {}".format(sim))