diff --git a/.github/workflows/caravel_cocotb.yml b/.github/workflows/caravel_cocotb.yml new file mode 100644 index 0000000..4903d22 --- /dev/null +++ b/.github/workflows/caravel_cocotb.yml @@ -0,0 +1,49 @@ +name: Caravel Cocotb CI + +on: + push: + branches: + - '*' + paths: + - 'cocotb/caravel_cocotb/CI/**' + - '.github/workflows/caravel_cocotb.yml' + - 'cocotb/caravel_cocotb/scripts/**' + - '!cocotb/caravel_cocotb/**/' # Exclude directories within the specified directory + + + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Install dependencies + run: | + cd $GITHUB_WORKSPACE/cocotb + python3 -m pip install --upgrade pip + pip install -r requirements.txt + python3 -m pip install --upgrade --no-cache-dir volare + + - name: Install caravel_cocotb + run: | + cd $GITHUB_WORKSPACE/cocotb + pip install . + + - name: Run main.py + run: | + cd $GITHUB_WORKSPACE/cocotb/caravel_cocotb/CI + python3 main.py diff --git a/cocotb/caravel_cocotb/CI/base_class.py b/cocotb/caravel_cocotb/CI/base_class.py new file mode 100644 index 0000000..3f05cdc --- /dev/null +++ b/cocotb/caravel_cocotb/CI/base_class.py @@ -0,0 +1,14 @@ +import logging + + +class BaseClass: + def __init__(self): + self.configure_logger() + + def configure_logger(self): + self.logger = logging.getLogger(f"{self.__class__.__name__}") + self.logger.setLevel(logging.INFO) + formatter = logging.Formatter("%(levelname)s@%(asctime)s: [%(name)s] %(message)s") + console_handler = logging.StreamHandler() + console_handler.setFormatter(formatter) + self.logger.addHandler(console_handler) \ No newline at end of file diff --git a/cocotb/caravel_cocotb/CI/checker.py b/cocotb/caravel_cocotb/CI/checker.py new file mode 100644 index 0000000..1f05495 --- /dev/null +++ b/cocotb/caravel_cocotb/CI/checker.py @@ -0,0 +1,169 @@ +from base_class import BaseClass +import yaml +import os +import xml.etree.ElementTree as ET + + +class Checker(BaseClass): + def __init__(self): + super().__init__() + pass + + def check_command(self, command): + self.command = command + self.logger.info(f"[Checker] Check test {self.command}") + all_tests_paths = self.get_expected_tests_dir() + is_pass = self.check_exist_pass(all_tests_paths) + if not is_pass: + # don't do other checks in sdf sim because it didn't run + return + self.check_configs() + self.check_seed(all_tests_paths) + self.check_dump_wave(all_tests_paths) + self.check_compile(all_tests_paths) + self.check_macros(all_tests_paths) + return + + def get_expected_tests_dir(self): + self.logger.debug(f"[get_expected_tests_dir] Checking tests exist and pass test = {self.command.test} testlist = {self.command.test_list} sim = {self.command.sim}") + if self.command.sim is None: + self.logger.debug("Sim is None") + sims = ["RTL"] + else: + sims = self.command.sim.split() + expected_tests = list() + # to get tests run by -t + if self.command.test is None: + self.logger.debug("Test is None") + tests = None + else: + tests = self.command.test.split() + for sim_type in sims: + for test_name in tests: + if sim_type != "GL_SDF": + expected_tests.append(f"{sim_type}-{test_name}") + else: + expected_tests.append(f"{sim_type}-{test_name}-{'nom-t' if self.command.corner is None else self.command.corner}") + # to get tests run by -tl + if self.command.test_list is not None: + with open(self.command.test_list, 'r') as yaml_file: + yaml_data = yaml_file.read() + tests = yaml.safe_load(yaml_data).get("Tests", []) + for test in tests: + test_name = test.get("name") + sim_type = test.get("sim") + if sim_type != "GL_SDF": + expected_tests.append(f"{sim_type}-{test_name}") + else: + expected_tests.append(f"{sim_type}-{test_name}-{'nom-t' if self.command.corner is None else self.command.corner}") + self.tag_path = f"{self.command.run_location if self.command.sim_path is None else self.command.sim_path}/sim/{self.command.tag}/" + full_tests_paths = [self.tag_path+expected_test for expected_test in expected_tests] + self.logger.info(f"[get_expected_tests_dir] Expected tests: {full_tests_paths}") + return full_tests_paths + + def check_exist_pass(self, all_tests_paths): + for test_path in all_tests_paths: + if os.path.exists(test_path): + self.logger.info(f"[check_exist_pass] Test {test_path} exists") + else: + raise ValueError(f"[check_exist_pass] Test {test_path} doesn't exist") + return False + + if os.path.exists(f"{test_path}/passed"): + self.logger.info(f"[check_exist_pass] Test {test_path} passed") + else: + if self.command.sim != "GL_SDF": + raise ValueError(f"[check_exist_pass] Test {test_path} failed") + return False + return True + + def check_configs(self): + # read design_info.yaml + design_info_path = self.command.design_info if self.command.design_info is not None else f"{self.command.run_location}/design_info.yaml" + with open(design_info_path, 'r') as yaml_file: + yaml_data = yaml_file.read() + design_info = yaml.safe_load(yaml_data) + caravel_root_exp = design_info.get("CARAVEL_ROOT") + mgmt_core_exp = design_info.get("MCW_ROOT") + pdk_root_exp = design_info.get("PDK_ROOT")+"/"+design_info.get("PDK") + pdk_exp = design_info.get("PDK")[:-1] + clk_exp = self.command.clk if self.command.clk is not None else design_info.get("clk") + max_err_exp = int(self.command.max_error) if self.command.max_error is not None else 3 + + # read configs.yaml generated + config_path = f"{self.tag_path}/configs.yaml" + with open(config_path, 'r') as yaml_file: + yaml_data = yaml_file.read() + configs = yaml.safe_load(yaml_data) + if clk_exp != configs.get("clock"): + raise ValueError(f"[check_configs] Clock mismatch: {clk_exp} != {configs.get('clock')}") + if max_err_exp != int(configs.get("max_err")): + raise ValueError(f"[check_configs] Max error mismatch: {max_err_exp} != {configs.get('max_err')}") + if caravel_root_exp != configs.get("CARAVEL_ROOT"): + raise ValueError(f"[check_configs] Caravel root mismatch: {caravel_root_exp} != {configs.get('CARAVEL_ROOT')}") + if mgmt_core_exp != configs.get("MCW_ROOT"): + raise ValueError(f"[check_configs] Management core mismatch: {mgmt_core_exp} != {configs.get('MCW_ROOT')}") + if pdk_root_exp != configs.get("PDK_ROOT"): + raise ValueError(f"[check_configs] PDK root mismatch: {pdk_root_exp} != {configs.get('PDK_ROOT')}") + if pdk_exp != configs.get("PDK"): + raise ValueError(f"[check_configs] PDK mismatch: {pdk_exp} != {configs.get('PDK')}") + + def check_seed(self, all_tests_paths): + if self.command.seed is not None: + for test_path in all_tests_paths: + seed_file_path = f"{test_path}/seed.xml" + with open(seed_file_path, 'r') as xml_file: + xml_content = xml_file.read() + + # Parse the XML content + root = ET.fromstring(xml_content) + + # Find the random_seed property and extract its value + seed_value = None + for testsuite in root.findall(".//testsuite"): + seed_elem = testsuite.find(".//property[@name='random_seed']") + if seed_elem is not None: + seed_value = int(seed_elem.get("value")) + break + if seed_value == self.command.seed: + self.logger.info(f"[check_seed] Test run with correct seed {seed_value}") + else: + raise ValueError(f"[check_seed] Test run with incorrect seed {seed_value} instead of {self.command.seed}") + + def check_dump_wave(self, all_tests_paths): + dump_wave_exp = False if self.command.no_wave is not None else True + for test_path in all_tests_paths: + if os.path.exists(f"{test_path}/waves.vcd") and not dump_wave_exp: + raise ValueError(f"[check_dump_wave] Test {test_path} dump waves while -no_wave switch is used") + elif not os.path.exists(f"{test_path}/waves.vcd") and dump_wave_exp: + raise ValueError(f"[check_dump_wave] Test {test_path} doesn't dump waves while -no_wave switch isn't used") + else: + self.logger.info(f"[check_dump_wave] Test {test_path} has wave {'dumped' if dump_wave_exp else 'not dumped'} waves as expected") + + def check_compile(self, all_tests_paths): + is_compile_shared = True if self.command.compile is None else False + for test_path in all_tests_paths: + simvpp_exist = os.path.exists(f"{test_path}/sim.vvp") + if simvpp_exist and is_compile_shared: + raise ValueError(f"[check_compile] Test {test_path} compile is not shared while -compile switch is used") + elif not simvpp_exist and not is_compile_shared: + raise ValueError(f"[check_compile] Test {test_path} shared compile while -compile switch isn't used simvpp exist = {simvpp_exist} is shared = {is_compile_shared}") + else: + self.logger.info(f"[check_compile] Test {test_path} has compile {'shared' if is_compile_shared else 'not shared'} as expected") + + def check_macros(self, all_tests_paths): + macro_used = self.command.macro + if macro_used is not None: + pattern_to_search = f"Found {macro_used} effect" + for test_path in all_tests_paths: + # search pattern in all .log files + for filename in os.listdir(test_path): + if filename.endswith('.log'): + file_log = os.path.join(test_path, filename) + with open(file_log, 'r') as log_file: + content = log_file.read() + if pattern_to_search in content: + self.logger.info(f"[check_macros] Test {test_path} uses macro {macro_used} correctly") + return True + raise ValueError(f"[check_macros] Test {test_path} doesn't use macro {macro_used}") + return False diff --git a/cocotb/caravel_cocotb/CI/gen_run_command.py b/cocotb/caravel_cocotb/CI/gen_run_command.py new file mode 100644 index 0000000..3ddb19f --- /dev/null +++ b/cocotb/caravel_cocotb/CI/gen_run_command.py @@ -0,0 +1,179 @@ +import os +from base_class import BaseClass +import random +import string +from collections import namedtuple +import yaml + +Command = namedtuple('Command', [ + 'test', 'test_list', 'design_info', 'sim', 'tag', + 'max_error', 'corner', 'seed', 'no_wave', 'clk', + 'macro', 'sim_path', 'verbosity', 'compile', 'check_commits', + "run_location", "CI"]) + + +class GenRunCommand(BaseClass): + def __init__(self, paths) -> None: + super().__init__() + self.paths = paths + self.cocotb_path = f"{self.paths.user_project_root}/verilog/dv/cocotb" + self.update_design_info() + self.rand_command = GenerateCommands(self.cocotb_path) + + def run_command(self, command): + str = command[1] + command_tuple = command[0] + self.logger.info(f"[run_command] command string: {str}") + self.logger.info(f"[run_command] command tuple: {command_tuple}") + os.system(str) + self.logger.info(f"[run_command] finished running command: {str}") + + def next_command(self): + return self.rand_command.next_command() + + def is_all_cases_covered(self): + # if all sim cases has been tested all other cases will be tested + # since sims are the biggest essential list until sdf sims added to the CI + return self.rand_command.sims_chooser.is_all_chosen + + def print_directory_tree(self, root_dir, indent=''): + if not os.path.isdir(root_dir): + self.logger.info("Invalid directory path.") + return + + items = sorted(os.listdir(root_dir)) + + for item in items: + item_path = os.path.join(root_dir, item) + if os.path.isdir(item_path): + self.logger.info(f"{indent}+ {item}/") + # self.print_directory_tree(item_path, indent + ' ') + else: + self.logger.info(f"{indent}- {item}") + + def update_design_info(self): + data = { + 'CARAVEL_ROOT': self.paths.caravel_root, + 'MCW_ROOT': self.paths.mgmt_core_root, + 'USER_PROJECT_ROOT': self.paths.user_project_root, + 'PDK_ROOT': self.paths.pdk_root, + 'PDK': "sky130A", + 'clk': 25, + 'caravan': False, + 'emailto': [None] + } + + with open(f'{self.paths.user_project_root}/verilog/dv/cocotb/design_info.yaml', 'w') as file: + yaml.dump(data, file) + + +class GenerateCommands(BaseClass): + def __init__(self, cocotb_path) -> None: + super().__init__() + self.cocotb_path = cocotb_path + self.all_switches_chooser() + + def all_switches_chooser(self): + self.tests_chooser = RandomChooser([None, "test_io10", "test_mgmt_gpio", "test_io10 test_mgmt_gpio"]) + self.test_list_chooser = RandomChooser([f"{self.cocotb_path}/test_io10/test_io10.yaml", f"{self.cocotb_path}/test_mgmt_gpio/test_mgmt_gpio.yaml"]) + self.design_infos_chooser = RandomChooser([None, f"{self.cocotb_path}/design_info.yaml"]) + self.sims_chooser = RandomChooser([None, 'RTL', "GL", "GL_SDF", "RTL GL", "GL RTL"]) + self.tags_chooser = RandomChooser([''.join(random.choice(string.ascii_lowercase) for _ in range(i)) for i in range(5,10)]) # change string length to make sure tag is unique + self.max_errors_chooser = RandomChooser([None, "7", "5", "100"]) + self.corners_chooser = RandomChooser([None, "nom-t", "nom-f", "nom-s", "max-t", "max-f", "max-s", "min-t", "min-f", "min-s"]) + self.seed_chooser = RandomChooser([None] + [random.randint(0, 100000) for _ in range(5)]) + self.no_wave_chooser = RandomChooser([None, None, True]) + self.clks_chooser = RandomChooser([None, 30, 40, 50]) + self.macros_chooser = RandomChooser([None, "USE_MACRO_1", "USE_MACRO_2"]) + self.sim_paths_chooser = RandomChooser([None, os.path.abspath(os.path.join(self.cocotb_path, ".."))]) + # self.verbosities_chooser = RandomChooser([None, "quiet", "normal", "debug"]) + self.verbosities_chooser = RandomChooser(["quiet"]) # to speed sims + self.compiles_chooser = RandomChooser([None, True]) + self.check_commits_chooser = RandomChooser([None, True]) + self.run_location = RandomChooser([self.cocotb_path, os.path.abspath(os.path.join(self.cocotb_path, "..", ".."))]) + + def next_command(self): + ################### constrains ############################ + test = self.tests_chooser.choose_next() + # force test_list to be None if test is provided and sim to be None if testlist is provided + if test is not None: + test_list = None + sim = self.sims_chooser.choose_next() + else: + test_list = self.test_list_chooser.choose_next() + sim = None + design_info = self.design_infos_chooser.choose_next() + # force run location to be in cocotb directory if design_info path file isn't provided + if design_info is None: + run_location = self.cocotb_path + else: + run_location = self.run_location.choose_next() + ######################################################### + + command = Command( + test=test, + test_list=test_list, + design_info=design_info, + sim=sim, + tag=self.tags_chooser.choose_next(), + max_error=self.max_errors_chooser.choose_next(), + corner=self.corners_chooser.choose_next(), + seed=self.seed_chooser.choose_next(), + no_wave=self.no_wave_chooser.choose_next(), + clk=self.clks_chooser.choose_next(), + macro=self.macros_chooser.choose_next(), + sim_path=self.sim_paths_chooser.choose_next(), + verbosity=self.verbosities_chooser.choose_next(), + compile=self.compiles_chooser.choose_next(), + check_commits=self.check_commits_chooser.choose_next(), + run_location=run_location, + CI=True + ) + # force run location to be in cocotb directory if design_info path file isn't provided + if command.design_info is None: + command._replace(run_location=f"{self.cocotb_path}") + # force test_list to be None if test is provided + if command.test is not None: + command._replace(test_list=None) + command_str = self.generate_command_str(command) + return (command, command_str) + + def generate_command_str(self, command): + test = f" -t {command.test} " if command.test is not None else f" -tl {command.test_list} " + design_info = f" -design_info {command.design_info} " if command.design_info is not None else "" + sim = f" -sim {command.sim} " if command.sim is not None else "" + max_error = f" -maxerr {command.max_error} " if command.max_error is not None else "" + corner = f" -corner {command.corner} " if command.corner is not None else "" + seed = f" -seed {command.seed} " if command.seed is not None else "" + no_wave = " -no_wave " if command.no_wave is not None else "" + clk = f" -clk {command.clk} " if command.clk is not None else "" + macro = f" -macro {command.macro} " if command.macro is not None else "" + sim_path = f" -sim_path {command.sim_path} " if command.sim_path is not None else "" + verbosity = f" -verbosity {command.verbosity} " if command.verbosity is not None else "" + compile = " -compile" if command.compile is not None else "" + CI = " --CI" if command.CI is not None else "" + check_commits = " -check_commits" if command.check_commits is not None else "" + # TODO for now remove using {check_commits} + command = f"cd {command.run_location} && caravel_cocotb {test}{design_info}{sim}{max_error}{corner}{seed}{no_wave}{clk}{macro}{sim_path}{verbosity}{compile}{CI} -tag {command.tag}" + return command + + + +class RandomChooser: + def __init__(self, items): + self.items = items.copy() + self.reset() + self.is_all_chosen = False + + def reset(self): + self.shuffled_items = self.items.copy() + random.shuffle(self.shuffled_items) + self.index = 0 + + def choose_next(self): + if self.index >= len(self.shuffled_items): + self.is_all_chosen = True + self.reset() + chosen_item = self.shuffled_items[self.index] + self.index += 1 + return chosen_item \ No newline at end of file diff --git a/cocotb/caravel_cocotb/CI/main.py b/cocotb/caravel_cocotb/CI/main.py new file mode 100644 index 0000000..48ab624 --- /dev/null +++ b/cocotb/caravel_cocotb/CI/main.py @@ -0,0 +1,31 @@ +from setup_env import SetupEnv +from gen_run_command import GenRunCommand +from checker import Checker +import os +from collections import namedtuple +DirectoryPaths = namedtuple('DirectoryPaths', ['caravel_root', 'mgmt_core_root', 'user_project_root', "pdk_root"]) + + +def main(): + try: + current_path = os.getcwd() + paths = DirectoryPaths( + caravel_root=f'{current_path}/caravel', + mgmt_core_root=f'{current_path}/mgmt_core', + user_project_root=f'{current_path}/user_project_root', + pdk_root=f'{current_path}/pdk_root' + ) + SetupEnv(paths) + gen_run_obj = GenRunCommand(paths) + checker_obj = Checker() + while gen_run_obj.is_all_cases_covered() is False: + command = gen_run_obj.next_command() + gen_run_obj.run_command(command) + checker_obj.check_command(command[0]) + except Exception as e: + print(f"Returned exception: {e}") + exit(1) + + +if __name__ == "__main__": + main() diff --git a/cocotb/caravel_cocotb/CI/setup_env.py b/cocotb/caravel_cocotb/CI/setup_env.py new file mode 100644 index 0000000..743c734 --- /dev/null +++ b/cocotb/caravel_cocotb/CI/setup_env.py @@ -0,0 +1,59 @@ +import os +import subprocess +import volare +from base_class import BaseClass + + +class SetupEnv(BaseClass): + def __init__(self, paths): + super().__init__() + self.paths = paths + self.clone_needed_repos() + self.pull_cocotb_docker() + + def clone_needed_repos(self): + self.clone_repo(repo_url="https://github.com/efabless/caravel.git", target_dir=self.paths.caravel_root, depth=1, branch="main") + self.clone_repo(repo_url="https://github.com/M0stafaRady/caravel_cocotb_tests.git", target_dir=self.paths.user_project_root, depth=1, branch="cocotb-CI") + self.clone_repo(repo_url="https://github.com/efabless/caravel_mgmt_soc_litex.git", target_dir=self.paths.mgmt_core_root, depth=1, branch="cooctb") + self.download_sky130_pdk("e3b630d9b7c0e23615367d52c4f78b2d2ede58ac") + + def pull_cocotb_docker(self): + image_name = 'efabless/dv' + tag = 'cocotb' + docker_pull_command = f"docker pull {image_name}:{tag}" + self.logger.info(f"Pulling cocotb docker image using command: {docker_pull_command}") + subprocess.run(docker_pull_command, shell=True, check=True) + + def download_sky130_pdk(self, pdk_version): + self.logger.info(f"download sky130 pdk with pdk version = {pdk_version}") + try: + volare.enable(pdk_root=self.paths.pdk_root, pdk="sky130", version=pdk_version) + except Exception as e: + raise RuntimeError(f"Error occurred while downloading pdk: {e}") + + def clone_repo(self, repo_url, target_dir, branch=None, commit=None, depth=None): + if not os.path.exists(target_dir): + os.makedirs(target_dir) + + clone_command = ["git", "clone"] + + if depth: + clone_command.extend(["--depth", str(depth)]) + + clone_command.extend([repo_url, target_dir]) + + if branch: + clone_command.extend(["--branch", branch]) + + self.logger.info(f"Cloning new repo using command: {clone_command}") + + try: + subprocess.run(clone_command, check=True) + if commit: + self.checkout_commit(target_dir, commit) + except Exception as e: + raise RuntimeError(f"Error occurred while cloning repo: {e}") + + def checkout_commit(self, repo_dir, commit): + subprocess.run(["git", "checkout", commit], cwd=repo_dir, check=True) + diff --git a/cocotb/caravel_cocotb/__main__.py b/cocotb/caravel_cocotb/__main__.py index 337e75f..33224ca 100755 --- a/cocotb/caravel_cocotb/__main__.py +++ b/cocotb/caravel_cocotb/__main__.py @@ -72,11 +72,6 @@ def main(): "-verbosity", help='verbosity of the console output it can have one of 3 value debug, normal or quiet the default value is normal', ) - parser.add_argument( - "-openframe", - action="store_true", - help='use openframe for the simulation rather than caravel', - ) parser.add_argument( "-check_commits", action="store_true", @@ -93,6 +88,8 @@ def main(): help='force recompilation', ) parser.add_argument("--run_defaults", action="store_true", help=argparse.SUPPRESS) # this used to run the default tests the flag is hidden because it just used with caravel verification flow + parser.add_argument("--CI", action="store_true", help=argparse.SUPPRESS) #used only for CI to run docker in non interactive mode + parser.add_argument("-no_gen_defaults", action="store_true", help="don't run gen_gpio_defaults script") args = parser.parse_args() # Arguments = namedtuple("Arguments","regression test sim corner testlist tag maxerr vcs cov checker_en caravan emailto seed no_wave clk lint arm sdf_setup") diff --git a/cocotb/caravel_cocotb/docker/DockerFile b/cocotb/caravel_cocotb/docker/DockerFile new file mode 100644 index 0000000..ab0c73b --- /dev/null +++ b/cocotb/caravel_cocotb/docker/DockerFile @@ -0,0 +1,108 @@ +# FROM efabless/dv:latest +# FROM efabless/dv_setup:latest +# Use the latest Ubuntu as the base image +FROM ubuntu:22.04 + + +# Install essential packages +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y \ + python3 \ + python3-tk \ + python3-pip \ + git \ + help2man \ + perl \ + make \ + autoconf \ + g++ \ + flex \ + bison \ + ccache \ + libgoogle-perftools-dev \ + numactl \ + perl-doc \ + libfl2 \ + libfl-dev \ + zlib1g-dev\ + build-essential \ + wget\ + automake\ + autotools-dev\ + curl\ + libmpc-dev\ + libmpfr-dev\ + libgmp-dev\ + gawk\ + texinfo\ + libtool\ + patchutils\ + bc\ + libexpat-dev\ + ninja-build\ + cmake\ + libglib2.0-dev\ + gperf + +# Upgrade pip +RUN pip3 install --upgrade pip + +# Install Python packages using pip +RUN pip3 install --upgrade\ + cocotb \ + cocotb_coverage \ + cocotb-bus \ + coverage \ + loremipsum \ + oyaml \ + prettytable \ + anytree\ + caravel-cocotb + +# Install Verilator from source v5.012 +RUN git clone https://github.com/verilator/verilator && \ + cd verilator && \ + git pull && \ + git checkout v5.012 && \ + autoconf && \ + ./configure && \ + make -j $(nproc) && \ + make install && \ + cd .. && rm -rf verilator + + +# Clone and build open-src-cvc commit b3e7fded6d4d79491886de40aec3a780efdd9d4e +RUN git clone https://github.com/cambridgehackers/open-src-cvc.git && \ + cd open-src-cvc/ && git checkout b3e7fded6d4d79491886de40aec3a780efdd9d4e && \ + cd src && make -f makefile.cvc64 && \ + cp cvc64 /bin/cvc64 && \ + cd ../.. && rm -rf open-src-cvc + +# Clone and build iverilog v12 +RUN git clone https://github.com/steveicarus/iverilog.git && cd iverilog && \ + git checkout v12_0 && sh autoconf.sh && \ + ./configure && make && make install && \ + cd .. && rm -rf iverilog + +# clone and install RISC-V toolchain v 2023.07.07 +RUN git clone https://github.com/riscv/riscv-gnu-toolchain && \ + cd riscv-gnu-toolchain && git checkout 2023.07.07 && \ + ./configure --prefix=/opt/riscv --enable-multilib --with-arch=rv32gc --with-abi=ilp32d && \ + make linux && \ + cd .. && rm -rf riscv-gnu-toolchain + +# Add toolchain to PATH +ENV PATH="/opt/riscv/bin:${PATH}" + + + +# Clean up unnecessary files and packages +RUN apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# Set the working directory +WORKDIR /app + +# Default command when the container starts +CMD ["bash"] + diff --git a/cocotb/caravel_cocotb/docker/README.rst b/cocotb/caravel_cocotb/docker/README.rst new file mode 100644 index 0000000..9f38f6f --- /dev/null +++ b/cocotb/caravel_cocotb/docker/README.rst @@ -0,0 +1,66 @@ +=========== +Dockerfile +=========== + +This Dockerfile provides instructions for building the `efabless/dv:cocotb` Docker image. This image includes various packages and tools for caravel Verification with vcs. + +Docker Image Contents +--------------------- + +The Docker image is based on Ubuntu 22.04 and includes the following packages and tools: + +- Python 3 and related libraries +- Git, Help2man, Perl, Make, Autoconf, G++, Flex, Bison, CCache, libgoogle-perftools-dev, Numactl, Perl-doc, libfl2, libfl-dev, zlib1g-dev, and other essential build tools +- Verilator v5.012 +- cvc (commit b3e7fded6d4d79491886de40aec3a780efdd9d4e) +- Icarus Verilog v12 +- RISC-V Toolchain v2023.07.07 + + +The image installs the following Python packages using pip: + +- `cocotb`: Coroutine-based cosimulation library for writing testbenches in Python. +- `cocotb-coverage`: A coverage analysis extension for cocotb. +- `cocotb-bus`: A library for generating bus transactions in cocotb testbenches. +- `coverage`: Code coverage measurement tool. +- `loremipsum`: A utility for generating random strings and text. +- `oyaml`: A YAML parser and emitter library. +- `prettytable`: A library for displaying tabular data in an ASCII table format. +- `anytree`: A library for building and navigating tree structures in Python. +- `caravel-cocotb`: Cocotb-based tests for the Caravel chip project. + + +Pulling the Docker Image +------------------------ + +You can pull the `efabless/dv:cocotb` Docker image from Docker Hub using the following command:: + + docker pull efabless/dv:cocotb + +Building the Docker Image +-------------------------- + +1. **Navigate to the directory containing this Dockerfile:** + + Navigate to this directory and build the Docker image using the following command:: + + docker build -t efabless/dv:cocotb . + + To build diffrent image extend the image name with new tag:: + + docker build -t efabless/dv:cocotb: . + + + +Pushing the Docker Image (if you have access) +----------------------------------------------- + +1. Log in to Docker Hub: + + Log in to Docker Hub using the docker login command. You will need to enter your Docker Hub credentials (username and password or token).:: + + docker login + +2. Push the image to Docker Hub:: + + push efabless/dv:cocotb diff --git a/cocotb/caravel_cocotb/interfaces/SPI.py b/cocotb/caravel_cocotb/interfaces/SPI.py index d22ec91..f0954d6 100644 --- a/cocotb/caravel_cocotb/interfaces/SPI.py +++ b/cocotb/caravel_cocotb/interfaces/SPI.py @@ -381,3 +381,4 @@ class SPI_COMMAND(IntEnum): WRITE_READ = 0xC0 USER_PASS_THRU = 0x02 MGMT_PATH_THRU = 0xC4 + diff --git a/cocotb/caravel_cocotb/interfaces/UART.py b/cocotb/caravel_cocotb/interfaces/UART.py index 0f0528a..6d71725 100644 --- a/cocotb/caravel_cocotb/interfaces/UART.py +++ b/cocotb/caravel_cocotb/interfaces/UART.py @@ -60,10 +60,14 @@ async def get_char(self): return chr(int(char, 2)) async def start_of_tx(self): - await FallingEdge(self.caravelEnv.dut._id(f"gpio{self.uart_pins['tx']}_monitor",False)) - await Timer(self.bit_time_ns, units="ns") - await NextTimeStep() - await NextTimeStep() + while True: + await FallingEdge(self.caravelEnv.dut._id(f"gpio{self.uart_pins['tx']}_monitor", False)) + await Timer(2, units="ns") + if self.caravelEnv.dut._id(f"gpio{self.uart_pins['tx']}_monitor", False).value == 1: + continue # to skip latches + await Timer(self.bit_time_ns - 2, units="ns") + await Timer(int(self.bit_time_ns/2), units="ns") # read the bit from the middle + break async def uart_send_char(self, char): """Send character to UART (character is sent to the software) diff --git a/cocotb/caravel_cocotb/interfaces/caravel.py b/cocotb/caravel_cocotb/interfaces/caravel.py index 7cee4a3..6009bc6 100644 --- a/cocotb/caravel_cocotb/interfaces/caravel.py +++ b/cocotb/caravel_cocotb/interfaces/caravel.py @@ -62,9 +62,9 @@ async def start_up(self): await self.reset() await self.disable_bins() - async def disable_bins(self): + async def disable_bins(self, ignore_bins=[3, 4]): for i in range(self.active_gpios_num): - if i in [3, 4]: # CSB and SCK + if i in ignore_bins: # CSB and SCK continue common.drive_hdl(self.dut._id(f"gpio{i}_en", False), (0, 0), 0) await ClockCycles(self.clk, 1) diff --git a/cocotb/caravel_cocotb/interfaces/common_functions/la.h b/cocotb/caravel_cocotb/interfaces/common_functions/la.h index 53a8757..dcfa031 100644 --- a/cocotb/caravel_cocotb/interfaces/common_functions/la.h +++ b/cocotb/caravel_cocotb/interfaces/common_functions/la.h @@ -25,9 +25,9 @@ enum la_reg_number { }; #endif //DOXYGEN_DOCS_ONLY /** - * Setting logic analyzer input enable + * Setting logic analyzer input enable. High to enable. * - * Enable as input to the user project. firmware sends to user project + * Enable as input from the user project to the firmware. * * @param reg_num logic analyzer register to write to. * Usually not all caravel versions has the same numbers of LA registers @@ -51,9 +51,9 @@ void LogicAnalyzer_inputEnable(enum la_reg_number reg_num , unsigned int is_enab } } /** - * Setting logic analyzer output enable + * Setting logic analyzer output enable. Low to enable. * - * Enable as output from the user project. firmware receives from user project + * Enable as output from the firmware to the user project. * * * @param reg_num logic analyzer register to write to. diff --git a/cocotb/caravel_cocotb/scripts/verify_cocotb/RunFlow.py b/cocotb/caravel_cocotb/scripts/verify_cocotb/RunFlow.py index a0f1712..b6dfb51 100644 --- a/cocotb/caravel_cocotb/scripts/verify_cocotb/RunFlow.py +++ b/cocotb/caravel_cocotb/scripts/verify_cocotb/RunFlow.py @@ -117,9 +117,6 @@ def set_paths(self, design_info): FIRMWARE_PATH = f"{design_info['MCW_ROOT']}/verilog/dv/fw" else: FIRMWARE_PATH = f"{design_info['MCW_ROOT']}/verilog/dv/firmware" - # For openframe as the cpu is inside the user project the firmware files should be inside user project as well - if self.args.openframe: - FIRMWARE_PATH = f"{design_info['USER_PROJECT_ROOT']}/verilog/dv/firmware" RUN_PATH = self.args.run_path SIM_PATH = ( f"{RUN_PATH}/sim" @@ -154,6 +151,8 @@ def set_cpu_type(self): def set_args(self, design_info): if self.args.clk is None: self.args.clk = design_info["clk"] + else: + self.args.clk = int(self.args.clk) if self.args.maxerr is None: self.args.maxerr = 3 @@ -234,12 +233,13 @@ def __init__( sim_path=None, run_path=".", verbosity="normal", - openframe=False, check_commits=False, design_info=None, no_docker=False, compile=False, - run_defaults=False + run_defaults=False, + CI=False, + no_gen_defaults=False ) -> None: self.test = test self.sim = sim @@ -261,12 +261,13 @@ def __init__( self.lint = None # related to repos self.cpu_type = None # would be filled by other class - self.openframe = openframe self.check_commits = check_commits self.design_info = design_info self.no_docker = no_docker self.compile = compile self.run_defaults = run_defaults + self.CI = CI + self.no_gen_defaults = no_gen_defaults def argparse_to_CocotbArgs(self, args): self.test = args.test @@ -286,9 +287,10 @@ def argparse_to_CocotbArgs(self, args): self.lint = args.lint self.run_path = os.getcwd() self.verbosity = args.verbosity - self.openframe = args.openframe self.check_commits = args.check_commits self.design_info = args.design_info self.no_docker = args.no_docker self.compile = args.compile - self.run_defaults = args.run_defaults \ No newline at end of file + self.run_defaults = args.run_defaults + self.CI = args.CI + self.no_gen_defaults = args.no_gen_defaults \ No newline at end of file diff --git a/cocotb/caravel_cocotb/scripts/verify_cocotb/RunRegression.py b/cocotb/caravel_cocotb/scripts/verify_cocotb/RunRegression.py index 2e6481c..0607836 100644 --- a/cocotb/caravel_cocotb/scripts/verify_cocotb/RunRegression.py +++ b/cocotb/caravel_cocotb/scripts/verify_cocotb/RunRegression.py @@ -4,7 +4,12 @@ import os import sys from subprocess import PIPE, run -from caravel_cocotb.scripts.verify_cocotb.RunTest import RunTest +try: + from os import path + sys.path.append(os.getcwd()) + from user_run_test import UserRunTest as RunTest +except ImportError: + from caravel_cocotb.scripts.verify_cocotb.RunTest import RunTest from caravel_cocotb.scripts.verify_cocotb.Test import Test from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText @@ -27,6 +32,7 @@ def __init__(self, args, paths, logger) -> None: self.get_tests() self.set_common_macros() self.unzip_sdf_files() + self.run_defaults_script() self.run_regression() self.send_mail() @@ -34,8 +40,6 @@ def set_common_macros(self): if self.args.macros is None: self.args.macros = list() simulation_macros = ["USE_POWER_PINS", "UNIT_DELAY=#1", "COCOTB_SIM"] - if self.args.openframe: - simulation_macros.append("OPENFRAME") paths_macros = [ f'RUN_PATH=\\"{self.paths.RUN_PATH}\\"', f'TAG=\\"{self.args.tag}\\"', @@ -154,7 +158,6 @@ def get_testlist(self, testlist_f): data["sim_type"] = test["sim"] if "corner" in test: data["corner"] = test["corner"] - if "macros" in test: data["macros"] = test["macros"] self.add_new_test(**data) @@ -162,6 +165,17 @@ def get_testlist(self, testlist_f): if data["sim_type"] not in self.args.sim: self.args.sim.append(data["sim_type"]) + def run_defaults_script(self): + if not ("GL_SDF" in self.args.sim or "GL" in self.args.sim): + return + if self.args.no_gen_defaults: + return + current_dir = os.getcwd() + os.chdir(f"{self.paths.CARAVEL_ROOT}/") + self.logger.info("Running gen_gpio_defaults script") + os.system(f"python3 scripts/gen_gpio_defaults.py {self.paths.USER_PROJECT_ROOT}") + os.chdir(current_dir) + def run_regression(self): # threads = list() for test in self.tests: @@ -182,20 +196,23 @@ def run_regression(self): # thread.join() # # Coverage if "RTL" in self.args.sim and self.args.vcs: - self.logger.info("\nStart merging coverage\n") - self.cov_dir = f"{self.paths.SIM_PATH}/{self.args.tag}/coverage" - # merge line coverage - old_path = os.getcwd() - os.chdir(f"{self.paths.SIM_PATH}/{self.args.tag}") - os.system(f"urg -dir */*.vdb -format both -show tests -report {self.cov_dir}/line_cov") - os.chdir(old_path) - # merge functional coverage - merge_fun_cov(f"{self.paths.SIM_PATH}/{self.args.tag}", reports_path=f"{self.cov_dir}/functional_cov") + try: + self.logger.info("\nStart merging coverage\n") + self.cov_dir = f"{self.paths.SIM_PATH}/{self.args.tag}/coverage" + # merge line coverage + old_path = os.getcwd() + os.chdir(f"{self.paths.SIM_PATH}/{self.args.tag}") + os.system(f"urg -dir */*.vdb -format both -show tests -report {self.cov_dir}/line_cov") + os.chdir(old_path) + # merge functional coverage + merge_fun_cov(f"{self.paths.SIM_PATH}/{self.args.tag}", reports_path=f"{self.cov_dir}/functional_cov") + except Exception as e: + self.logger.error(e) def test_run_function(self, test): test.start_of_test() self.update_run_log() - RunTest(self.args, self.paths, test, self.logger) + RunTest(self.args, self.paths, test, self.logger).run_test() self.update_run_log() def update_run_log(self): @@ -395,7 +412,11 @@ def unzip_sdf_files(self): sdf_dir = f"{self.paths.CARAVEL_ROOT}/signoff/{'caravan' if self.args.caravan else 'caravel'}/primetime/sdf" sdf_user_dir = f"{self.paths.USER_PROJECT_ROOT}/signoff/{'caravan' if self.args.caravan else 'caravel'}/primetime/sdf" - sdf_user_project = f"{self.paths.USER_PROJECT_ROOT}/signoff/user_project_wrapper/primetime/sdf" + user_project_name = "user_project_wrapper" + sdf_user_project = f"{self.paths.USER_PROJECT_ROOT}/signoff/{user_project_name}/primetime/sdf" + if not os.path.exists(sdf_user_project): # so special case for openframe maybe change it in the future + user_project_name = "openframe_project_wrapper" + sdf_user_project = f"{self.paths.USER_PROJECT_ROOT}/signoff/{user_project_name}/primetime/sdf" # check if user sdf dir exists if os.path.exists(sdf_user_dir) and os.path.isdir(sdf_user_dir) and len(os.listdir(sdf_user_dir)) > 0: @@ -406,7 +427,7 @@ def unzip_sdf_files(self): start_time = time.time() sdf_prefix1 = f"{corner[-1]}{corner[-1]}" sdf_prefix2 = f"{corner[0:3]}" - output_files = [f"{sdf_dir}/{sdf_prefix1}/{'caravan' if self.args.caravan else 'caravel'}.{sdf_prefix2}.sdf",f"{sdf_user_project}/{sdf_prefix1}/user_project_wrapper.{sdf_prefix2}.sdf"] + output_files = [f"{sdf_dir}/{sdf_prefix1}/{'caravan' if self.args.caravan else 'caravel'}.{sdf_prefix2}.sdf",f"{sdf_user_project}/{sdf_prefix1}/{user_project_name}.{sdf_prefix2}.sdf"] for output_file in output_files: compress_file = output_file + ".gz" # delete output file if exists diff --git a/cocotb/caravel_cocotb/scripts/verify_cocotb/RunTest.py b/cocotb/caravel_cocotb/scripts/verify_cocotb/RunTest.py index 6154067..002010d 100644 --- a/cocotb/caravel_cocotb/scripts/verify_cocotb/RunTest.py +++ b/cocotb/caravel_cocotb/scripts/verify_cocotb/RunTest.py @@ -3,7 +3,6 @@ import subprocess from caravel_cocotb.scripts.verify_cocotb.read_defines import GetDefines import re -import time import logging import caravel_cocotb @@ -14,20 +13,25 @@ def __init__(self, args, paths, test, logger) -> None: self.paths = paths self.test = test self.logger = logger + + def run_test(self): if self.hex_generate() == "hex_generated": # run test only if hex is generated self.runTest() - # self.runTest() self.test.end_of_test() - def hex_riscv32_command_gen(self): - GCC_PATH = "/foss/tools/riscv-gnu-toolchain-rv32i/217e7f3debe424d61374d31e33a091a630535937/bin/" + def docker_command_str(self, docker_image="efabless/dv:cocotb", docker_dir="", env_vars="", addtional_switchs="", command=""): + command = f"docker run {' --init -it --sig-proxy=true ' if not self.args.CI else ' ' } -u $(id -u $USER):$(id -g $USER) {addtional_switchs} {env_vars} {docker_dir} {docker_image} sh -ec '{command}'" + return command + + def hex_riscv_command_gen(self): + GCC_PATH = "/opt/riscv/bin/" GCC_PREFIX = "riscv32-unknown-linux-gnu" GCC_COMPILE = f"{GCC_PATH}/{GCC_PREFIX}" - SOURCE_FILES = ( - f"{self.paths.FIRMWARE_PATH}/crt0_vex.S {self.paths.FIRMWARE_PATH}/isr.c" - ) if not self.args.openframe else f"{self.paths.FIRMWARE_PATH}/start.s" + SOURCE_FILES = f"{self.paths.FIRMWARE_PATH}/crt0_vex.S {self.paths.FIRMWARE_PATH}/isr.c" + LINKER_SCRIPT = f"-Wl,-Bstatic,-T,{self.test.linker_script_file},--strip-debug " - CPUFLAGS = "-g -march=rv32i -mabi=ilp32 -D__vexriscv__ -ffreestanding -nostdlib" + CPUFLAGS = "-O2 -g -march=rv32i_zicsr -mabi=ilp32 -D__vexriscv__ -ffreestanding -nostdlib" + # CPUFLAGS = "-O2 -g -march=rv32imc_zicsr -mabi=ilp32 -D__vexriscv__ -ffreestanding -nostdlib" includes = f" -I{self.paths.FIRMWARE_PATH} -I{self.paths.FIRMWARE_PATH}/APIs -I{self.paths.VERILOG_PATH}/dv/generated -I{self.paths.VERILOG_PATH}/dv/ -I{self.paths.VERILOG_PATH}/common" includes += f" -I{self.paths.USER_PROJECT_ROOT}/verilog/dv/cocotb " elf_command = ( @@ -44,7 +48,7 @@ def hex_arm_command_gen(self): SOURCE_FILES = f"{self.paths.FIRMWARE_PATH}/cm0_start.s" LINKER_SCRIPT = f"-T {self.test.linker_script_file}" CPUFLAGS = "-O2 -Wall -nostdlib -nostartfiles -ffreestanding -mcpu=cortex-m0 -Wno-unused-value" - includes = f"-I{self.paths.FIRMWARE_PATH}" + includes = f"-I{self.paths.FIRMWARE_PATH} -I{self.paths.USER_PROJECT_ROOT}/verilog/dv/cocotb" elf_command = ( f"{GCC_COMPILE}-gcc {includes} {CPUFLAGS} {LINKER_SCRIPT}" f" -o {self.hex_dir}/{self.test.name}.elf {SOURCE_FILES} {self.c_file}" @@ -54,49 +58,29 @@ def hex_arm_command_gen(self): sed_command = f'sed -ie "s/@10/@00/g" {self.hex_dir}/{self.test.name}.hex' return f" {elf_command} &&{lst_command}&& {hex_command}&& {sed_command}" - def hex_generate(self): - # open docker + def hex_generate(self) -> str: + # get the test path from dv/cocotb test_path = self.test_path() + # Create a new hex_files directory because it does not exist if not os.path.exists(f"{self.paths.SIM_PATH}/hex_files"): - os.makedirs( - f"{self.paths.SIM_PATH}/hex_files" - ) # Create a new hex_files directory because it does not exist + os.makedirs(f"{self.paths.SIM_PATH}/hex_files") self.hex_dir = f"{self.paths.SIM_PATH}/hex_files/" self.c_file = f"{test_path}/{self.test.name}.c" - test_dir = f"{self.paths.VERILOG_PATH}/dv/tests-caravel/mem" # linker script include // TODO: to fix this in the future from the mgmt repo if self.args.cpu_type == "ARM": command = self.hex_arm_command_gen() else: - command = self.hex_riscv32_command_gen() + command = self.hex_riscv_command_gen() docker_dir = f"-v {self.hex_dir}:{self.hex_dir} -v {self.paths.RUN_PATH}:{self.paths.RUN_PATH} -v {self.paths.CARAVEL_ROOT}:{self.paths.CARAVEL_ROOT} -v {self.paths.MCW_ROOT}:{self.paths.MCW_ROOT} -v {self.test.test_dir}:{self.test.test_dir} " - docker_dir = (docker_dir+ f"-v {self.paths.USER_PROJECT_ROOT}:{self.paths.USER_PROJECT_ROOT}") - docker_command = f"docker run --init -u $(id -u $USER):$(id -g $USER) -it --sig-proxy=true {docker_dir} efabless/dv:latest sh -ec 'cd {test_dir} && {command} '" - # command_slipt = command.split("&&") - # self.firmware_log = open(self.test.hex_log, "w") - # self.firmware_log.write("docker for hex command:\n% ") - # self.firmware_log.write(os.path.expandvars(docker_command) + "\n\n") - # self.firmware_log.write("elf file generation command:\n% ") - # self.firmware_log.write(os.path.expandvars(command_slipt[0]) + "\n\n") - # self.firmware_log.write("lst file generation command:\n% ") - # self.firmware_log.write(os.path.expandvars(command_slipt[1]) + "\n\n") - # self.firmware_log.write("hex file generation command:\n% ") - # self.firmware_log.write(os.path.expandvars(command_slipt[2]) + "\n\n") - # self.firmware_log.close() + docker_dir = (docker_dir + f"-v {self.paths.USER_PROJECT_ROOT}:{self.paths.USER_PROJECT_ROOT}") + docker_command = self.docker_command_str(docker_image="efabless/dv:cocotb", docker_dir=docker_dir, command=command) # don't run with docker with arm cmd = command if self.args.cpu_type == "ARM" else docker_command - hex_gen_state = run_command_write_to_file(cmd, self.test.hex_log, self.logger, quiet=False if self.args.verbosity == "debug" else True) - # docker_process = subprocess.Popen(cmd, shell=True,stdout=subprocess.PIPE, stderr=subprocess.PIPE) - # docker_process.wait() - # stdout, _ = docker_process.communicate() - # ansi_escape = re.compile(r'\x1B[@-_][0-?]*[ -/]*[@-~]') - # stdout = ansi_escape.sub('', stdout.decode('utf-8')) - # self.firmware_log.write(stdout) - # hex_gen_state = docker_process.returncode + hex_gen_state = self.run_command_write_to_file(cmd, self.test.hex_log, self.logger, quiet=False if self.args.verbosity == "debug" else True) self.firmware_log = open(self.test.hex_log, "a") if hex_gen_state != 0: # open(self.test.firmware_log, "w").write(stdout) - self.logger.error( + raise RuntimeError( f"{bcolors.FAIL}Error:{bcolors.ENDC} Fail to compile the C code for more info refer to {bcolors.OKCYAN }{self.test.hex_log}{bcolors.ENDC } " ) self.firmware_log.write("Error: when generating hex") @@ -108,12 +92,12 @@ def hex_generate(self): shutil.copyfile(f"{self.test.hex_dir}/{self.test.name}.hex", f"{self.test.test_dir}/firmware.hex") return "hex_generated" - def test_path(self): - test_name = self.test.name - test_name += ".c" - tests_path = os.path.abspath(f"{self.paths.RUN_PATH}/tests") + def test_path(self, test_name=None): + if test_name is None: + test_name = self.test.name + c_file_name = test_name + ".c" tests_path_user = os.path.abspath(f"{self.paths.USER_PROJECT_ROOT}/verilog/dv/cocotb") - test_file = self.find(test_name, tests_path_user) + test_file = self.find(c_file_name, tests_path_user) test_path = os.path.dirname(test_file) return test_path @@ -127,24 +111,29 @@ def runTest(self): # iverilog function def runTest_iverilog(self): if self.test.sim == "GL_SDF": - self.logger.error( + raise RuntimeError( f"iverilog can't run SDF for test {self.test.name} Please use anothor simulator like cvc" ) return - self.test.set_user_project() + self.write_iverilog_includes_file() if not os.path.isfile(f"{self.test.compilation_dir}/sim.vvp") or self.args.compile: self.iverilog_compile() self.iverilog_run() + def write_iverilog_includes_file(self): + self.iverilog_dirs = " " + self.iverilog_dirs += f'-I {self.paths.USER_PROJECT_ROOT}/verilog/rtl' + self.test.set_user_project() + def iverilog_compile(self): macros = " -D" + " -D".join(self.test.macros) compile_command = ( f"cd {self.test.compilation_dir} &&" - f"iverilog -Ttyp {macros} -o {self.test.compilation_dir}/sim.vvp" + f"iverilog -g2012 -Ttyp {macros} {self.iverilog_dirs} -o {self.test.compilation_dir}/sim.vvp" f" {self.paths.CARAVEL_VERILOG_PATH}/rtl/toplevel_cocotb.v -s caravel_top" ) - docker_compilation_command = self.docker_command_str() + f" '{compile_command}' " - run_command_write_to_file( + docker_compilation_command = self._iverilog_docker_command_str(compile_command) + self.run_command_write_to_file( docker_compilation_command if not self.args.no_docker else compile_command, self.test.compilation_log, self.logger, @@ -155,15 +144,15 @@ def iverilog_run(self): defines = GetDefines(self.test.includes_file) seed = "" if self.args.seed is None else f"RANDOM_SEED={self.args.seed}" run_command = (f"cd {self.test.test_dir} && TESTCASE={self.test.name} MODULE=module_trail {seed} vvp -M $(cocotb-config --prefix)/cocotb/libs -m libcocotbvpi_icarus {self.test.compilation_dir}/sim.vvp +{ ' +'.join(self.test.macros) } {' '.join([f'+{k}={v}' if v != ''else f'+{k}' for k, v in defines.defines.items()])}") - docker_run_command = self.docker_command_str() + f" '{run_command}' " - run_command_write_to_file( + docker_run_command = self._iverilog_docker_command_str(run_command) + self.run_command_write_to_file( docker_run_command if not self.args.no_docker else run_command, None if self.args.verbosity == "quiet" else self.test.test_log2, self.logger, quiet=True if self.args.verbosity == "quiet" else False ) - def docker_command_str(self): + def _iverilog_docker_command_str(self, command=""): """the docker command without the command that would run""" env_vars = f"-e COCOTB_RESULTS_FILE={os.getenv('COCOTB_RESULTS_FILE')} -e CARAVEL_PATH={self.paths.CARAVEL_PATH} -e CARAVEL_VERILOG_PATH={self.paths.CARAVEL_VERILOG_PATH} -e VERILOG_PATH={self.paths.VERILOG_PATH} -e PDK_ROOT={self.paths.PDK_ROOT} -e PDK={self.paths.PDK} -e USER_PROJECT_VERILOG={self.paths.USER_PROJECT_ROOT}/verilog" local_caravel_cocotb_path = caravel_cocotb.__file__.replace("__init__.py", "") @@ -173,58 +162,48 @@ def docker_command_str(self): if os.path.exists("/mnt/scratch/"): docker_dir += " -v /mnt/scratch/cocotb_runs/:/mnt/scratch/cocotb_runs/ " display = " -e DISPLAY=$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix -v $HOME/.Xauthority:/.Xauthority --network host --security-opt seccomp=unconfined " - command = f"docker run --init -u $(id -u $USER):$(id -g $USER) -it --sig-proxy=true {display} {env_vars} {docker_dir} efabless/dv:cocotb sh -ec " + command = self.docker_command_str(docker_image="efabless/dv:cocotb", docker_dir=docker_dir, env_vars=env_vars, addtional_switchs=display, command=command) return command # vcs function def runTest_vcs(self): - dirs = f'+incdir+\\"{self.paths.PDK_ROOT}/{self.paths.PDK}\\" ' - if self.test.sim == "RTL": - shutil.copyfile( - f"{self.paths.VERILOG_PATH}/includes/{'openframe_vcs.v' if self.args.openframe else 'rtl_caravel_vcs.v'}", - f"{self.test.compilation_dir}/includes.v", - ) - else: - shutil.copyfile( - f"{self.paths.VERILOG_PATH}/includes/{'openframe_vcs.v' if self.args.openframe else 'gl_caravel_vcs.v'}", - f"{self.test.compilation_dir}/includes.v", - ) - if self.test.sim == "GL_SDF": - dirs += f'+incdir+\\"{self.paths.MCW_ROOT}/verilog/\\" ' - change_str( - str='"caravel/verilog', - new_str=f'"{self.paths.CARAVEL_PATH}', - file_path=f"{self.test.compilation_dir}/includes.v", - ) - change_str( - str='"caravel_mgmt_soc_litex/verilog', - new_str=f'"{self.paths.VERILOG_PATH}', - file_path=f"{self.test.compilation_dir}/includes.v", - ) - macros = " +define+" + " +define+".join(self.test.macros) - coverage_command = '' + self.write_vcs_includes_file() + self.vcs_coverage_command = '' if self.test.sim == "RTL": - coverage_command = "-cm line+tgl+cond+fsm+branch+assert " + self.vcs_coverage_command = "-cm line+tgl+cond+fsm+branch+assert " os.environ["TESTCASE"] = f"{self.test.name}" os.environ["MODULE"] = "module_trail" + if not os.path.isfile(f"{self.test.compilation_dir}/simv") or self.args.compile: + self.vcs_compile() + self.vcs_run() + + def write_vcs_includes_file(self): + # self.vcs_dirs = f'+incdir+\\"{self.paths.PDK_ROOT}/{self.paths.PDK}\\" ' + self.vcs_dirs = ' ' + if self.test.sim == "RTL": + self.vcs_dirs += f'+incdir+\\"{self.paths.USER_PROJECT_ROOT}/verilog/rtl\\" ' + self.test.set_user_project() + + def vcs_compile(self): + macros = " +define+" + " +define+".join(self.test.macros) if self.args.seed is not None: os.environ["RANDOM_SEED"] = self.args.seed - self.test.set_user_project() - defines = GetDefines(self.test.includes_file) - vlogan_cmd = f"cd {self.test.compilation_dir}; vlogan -full64 -sverilog +error+30 {self.paths.CARAVEL_VERILOG_PATH}/rtl/toplevel_cocotb.v {dirs} {macros} -l {self.test.compilation_dir}/analysis.log -o {self.test.compilation_dir} " - if not os.path.isfile(f"{self.test.compilation_dir}/simv") or self.args.compile: - run_command_write_to_file(vlogan_cmd, self.test.compilation_log, self.logger, quiet=False if self.args.verbosity == "debug" else True) + vlogan_cmd = f"cd {self.test.compilation_dir}; vlogan -full64 -sverilog +error+30 {self.paths.CARAVEL_VERILOG_PATH}/rtl/toplevel_cocotb.v {self.vcs_dirs} {macros} -l {self.test.compilation_dir}/analysis.log -o {self.test.compilation_dir} " + self.run_command_write_to_file(vlogan_cmd, self.test.compilation_log, self.logger, quiet=False if self.args.verbosity == "debug" else True) lint = "+lint=all" if self.args.lint else "" - vcs_cmd = f"cd {self.test.compilation_dir}; vcs {lint} {coverage_command} -debug_access+all +error+50 +vcs+loopreport+1000000 -diag=sdf:verbose +sdfverbose +neg_tchk -debug_access -full64 -l {self.test.compilation_dir}/test_compilation.log caravel_top -Mdir={self.test.compilation_dir}/csrc -o {self.test.compilation_dir}/simv +vpi -P pli.tab -load $(cocotb-config --lib-name-path vpi vcs)" - if not os.path.isfile(f"{self.test.compilation_dir}/simv") or self.args.compile: - run_command_write_to_file( - vcs_cmd, - self.test.compilation_log, - self.logger, - quiet=False if self.args.verbosity == "debug" else True - ) - run_sim = f"cd {self.test.test_dir}; {self.test.compilation_dir}/simv {coverage_command} -cm_name {self.test.name} +{ ' +'.join(self.test.macros)} {' '.join([f'+{k}={v}' if v != ''else f'+{k}' for k, v in defines.defines.items()])}" - run_command_write_to_file( + ignored_errors = " -error=noZMMCM " + vcs_cmd = f"cd {self.test.compilation_dir}; vcs {lint} {self.vcs_coverage_command} {ignored_errors}-debug_access+all +error+50 +vcs+loopreport+1000000 -diag=sdf:verbose +sdfverbose +neg_tchk -debug_access -full64 -l {self.test.compilation_dir}/test_compilation.log caravel_top -Mdir={self.test.compilation_dir}/csrc -o {self.test.compilation_dir}/simv +vpi -P pli.tab -load $(cocotb-config --lib-name-path vpi vcs)" + self.run_command_write_to_file( + vcs_cmd, + self.test.compilation_log, + self.logger, + quiet=False if self.args.verbosity == "debug" else True + ) + + def vcs_run(self): + defines = GetDefines(self.test.includes_file) + run_sim = f"cd {self.test.test_dir}; {self.test.compilation_dir}/simv +vcs+dumpvars+all {self.vcs_coverage_command} -cm_name {self.test.name} +{ ' +'.join(self.test.macros)} {' '.join([f'+{k}={v}' if v != ''else f'+{k}' for k, v in defines.defines.items()])}" + self.run_command_write_to_file( run_sim, None if self.args.verbosity == "quiet" else self.test.test_log2, self.logger, @@ -235,58 +214,45 @@ def find(self, name, path): for root, dirs, files in os.walk(path): if name in files: return os.path.join(root, name) - self.logger.error(f"Test {name} doesn't exist or don't have a C file ") - - -def run_command_write_to_file(cmd, file, logger, quiet=True): - """run command and write output to file return 0 if no error""" - if file is not None: - logger_file = logging.getLogger(file) - logger_file.setLevel(logging.INFO) - # Configure file handler for the logger - file_handler = logging.FileHandler(file) - file_handler.setLevel(logging.INFO) - file_formatter = logging.Formatter('%(message)s') - file_handler.setFormatter(file_formatter) - logger_file.addHandler(file_handler) - f = open(file, "a") - f.write("command:") - f.write(os.path.expandvars(cmd) + "\n\n") - f.close() - try: - process = subprocess.Popen( - cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=1024 - ) - while True: - out = process.stdout.readline().decode("utf-8") - ansi_escape = re.compile(r"\x1B[@-_][0-?]*[ -/]*[@-~]") - stdout = ansi_escape.sub("", out) - if process.poll() is not None: - break - if out: - if not quiet: - logger.info(stdout.replace("\n", "", 1)) - # time.sleep(0.01) - if file is not None: - logger_file.info(stdout.replace("\n", "", 1)) - except Exception as e: - logger(f"Docker process stopped by user {e}") - process.stdin.write(b'\x03') # Send the Ctrl+C signal to the Docker process - process.terminate() - - return process.returncode - - -def capture_output(process, file, logger): - for line in iter(process.stdout.readline, b""): - file.write(line.decode()) - logger.info(line.decode(), end="") + raise RuntimeError(f"Test {name} doesn't exist or don't have a C file ") + def run_command_write_to_file(self, cmd, file, logger, quiet=True): + """run command and write output to file return 0 if no error""" + if file is not None: + logger_file = logging.getLogger(file) + logger_file.setLevel(logging.INFO) + # Configure file handler for the logger + file_handler = logging.FileHandler(file) + file_handler.setLevel(logging.INFO) + file_formatter = logging.Formatter('%(message)s') + file_handler.setFormatter(file_formatter) + logger_file.addHandler(file_handler) + f = open(file, "a") + f.write("command:") + f.write(os.path.expandvars(cmd) + "\n\n") + f.close() + try: + process = subprocess.Popen( + cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=1024 + ) + while True: + out = process.stdout.readline().decode("utf-8") + ansi_escape = re.compile(r"\x1B[@-_][0-?]*[ -/]*[@-~]") + stdout = ansi_escape.sub("", out) + if process.poll() is not None: + break + if out: + if not quiet: + logger.info(stdout.replace("\n", "", 1)) + # time.sleep(0.01) + if file is not None: + logger_file.info(stdout.replace("\n", "", 1)) + except Exception as e: + logger(f"Docker process stopped by user {e}") + process.stdin.write(b'\x03') # Send the Ctrl+C signal to the Docker process + process.terminate() -def capture_error(process, file, logger): - for line in iter(process.stderr.readline, b""): - file.write(line.decode()) - logger.info(line.decode(), end="") + return process.returncode class bcolors: diff --git a/cocotb/caravel_cocotb/scripts/verify_cocotb/Test.py b/cocotb/caravel_cocotb/scripts/verify_cocotb/Test.py index 67e96ef..c1e13c6 100644 --- a/cocotb/caravel_cocotb/scripts/verify_cocotb/Test.py +++ b/cocotb/caravel_cocotb/scripts/verify_cocotb/Test.py @@ -76,18 +76,8 @@ def set_user_project(self): user_include = f"{self.paths.USER_PROJECT_ROOT}/verilog/includes/includes.rtl.caravel_user_project" else: user_include = f"{self.paths.USER_PROJECT_ROOT}/verilog/includes/includes.gl.caravel_user_project" - user_project = f" -f {user_include}" self.write_includes_file(user_include) - if self.args.vcs: - user_project = "" - lines = open(user_include, "r").readlines() - for line in lines: - if line.startswith("-v"): - user_project += line.replace( - "$(USER_PROJECT_VERILOG)", - f"{self.paths.USER_PROJECT_ROOT}/verilog", - ) + " " return user_project.replace("\n", "") def start_of_test(self): @@ -139,8 +129,8 @@ def create_logs(self): self.test_dir = f"{self.paths.SIM_PATH}/{self.args.tag}/{self.full_name}" if self.local_macros is not None or self.args.compile: self.compilation_dir = self.test_dir - else: - self.compilation_dir = f"{self.paths.SIM_PATH}/{self.args.tag}/compilation" + else: + self.compilation_dir = f"{self.paths.SIM_PATH}/{self.args.tag}/{self.sim}-compilation{f'-{self.corner}' if self.sim=='GL_SDF' else ''}" # remove if already exists if os.path.isdir(self.test_dir): shutil.rmtree(self.test_dir) @@ -284,29 +274,27 @@ def write_includes_file(self, file): paths = self.convert_list_to_include(file) # write to include file in the top of the file self.includes_file = f"{self.compilation_dir}/includes.v" - if self.args.vcs: - includes = open(self.includes_file, 'r').read() - else: - if self.sim == "RTL": - includes = self.convert_list_to_include(f"{self.paths.VERILOG_PATH}/includes/{'includes.rtl.caravel' if not self.args.openframe else 'includes.rtl.openframe'}") - elif self.sim == "GL": - includes = self.convert_list_to_include(f"{self.paths.VERILOG_PATH}/includes/includes.gl.caravel") + if self.sim == "RTL": + includes = self.convert_list_to_include(f"{self.paths.VERILOG_PATH}/includes/includes.rtl.caravel") + elif self.sim == "GL_SDF": + includes = self.convert_list_to_include(f"{self.paths.VERILOG_PATH}/includes/includes.gl+sdf.caravel") + elif self.sim == "GL": + includes = self.convert_list_to_include(f"{self.paths.VERILOG_PATH}/includes/includes.gl.caravel") includes = paths + includes open(self.includes_file, "w").write(includes) move_defines_to_start(self.includes_file, 'defines.v"') - - if self.args.iverilog: - # when running with iverilog add includes list also - paths = open(file, "r").read() - self.includes_list = f"{self.compilation_dir}/includes" - if self.sim == "RTL": - includes = open(f"{self.paths.VERILOG_PATH}/includes/{'includes.rtl.caravel' if not self.args.openframe else 'includes.rtl.openframe'}", 'r').read() - elif self.sim == "GL": - includes = open(f"{self.paths.VERILOG_PATH}/includes/includes.gl.caravel", 'r').read() - includes = paths + includes - open(self.includes_list, "w").write(includes) - move_defines_to_start(self.includes_list, 'defines.v') - + # copy includes used also + paths = open(file, "r").read() + self.includes_list = f"{self.compilation_dir}/includes" + if self.sim == "RTL": + includes = open(f"{self.paths.VERILOG_PATH}/includes/includes.rtl.caravel", 'r').read() + elif self.sim == "GL_SDF": + includes = open(f"{self.paths.VERILOG_PATH}/includes/includes.gl+sdf.caravel", 'r').read() + elif self.sim == "GL": + includes = open(f"{self.paths.VERILOG_PATH}/includes/includes.gl.caravel", 'r').read() + includes = paths + includes + open(self.includes_list, "w").write(includes) + move_defines_to_start(self.includes_list, 'defines.v') def convert_list_to_include(self, file): paths = "" diff --git a/cocotb/requirements.txt b/cocotb/requirements.txt index d0b38ee..ccaacda 100644 --- a/cocotb/requirements.txt +++ b/cocotb/requirements.txt @@ -1,3 +1,9 @@ -docker -cocotb -pyyaml \ No newline at end of file +click==8.0.3 +cocotb==1.8.0 +oyaml==1.0 +cocotb_coverage==1.1.0 +PrettyTable==3.8.0 +anytree==2.9.0 +PyYAML==6.0.1 +tabulate==0.9.0 + diff --git a/cocotb/setup.py b/cocotb/setup.py index dab23e8..fb7ff49 100644 --- a/cocotb/setup.py +++ b/cocotb/setup.py @@ -6,7 +6,7 @@ setup( name="caravel_cocotb", packages=find_packages(), - version="1.0.0", + version="1.2.3", description="efabless caravel cocotb verification flow.", long_description=open("README.md").read(), long_description_content_type="text/markdown", @@ -23,4 +23,4 @@ ], entry_points={"console_scripts": ["caravel_cocotb = caravel_cocotb.__main__:main"]}, python_requires=">3.6", -) \ No newline at end of file +)