| # 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)) |