Files
mlkem-sync/test_framework/lib/sim_controller.py
FallenSigh 8fdf944555 feat: init mlkem project with Verilator test framework
- sync_rtl/common/: skid_buffer, pipeline_reg, defines (valid/ready)
- sync_rtl/mod_add/: modular adder example with Verilator C++ TB
- test_framework/: Python-driven Verilator compile/sim/compare pipeline
- test_framework/modules/mod_add/: 50-vector test plan, full鏈路 PASS
- .trellis/spec/: RTL and test_framework conventions documented
2026-06-24 19:43:29 +08:00

135 lines
4.4 KiB
Python

"""sim_controller.py - Verilator simulation controller.
Handles Verilator compilation and execution of RTL testbenches.
"""
import subprocess
import os
import time
class SimController:
"""Controls Verilator compilation and simulation."""
def __init__(self, config: dict):
"""Initialize with test framework configuration.
Args:
config: The loaded config.json dict.
"""
self.config = config
self.verilator_path = config.get('verilator', {}).get('path', 'verilator')
self.compile_args = config.get('verilator', {}).get('compile_args', [])
def compile(self, top_module: str, rtl_files: list[str], tb_cpp: str,
work_dir: str, inc_dirs: list[str] = None) -> str:
"""Compile RTL with Verilator and return path to executable.
Args:
top_module: Name of the top-level Verilog module.
rtl_files: List of Verilog source file paths.
tb_cpp: Path to C++ testbench file.
work_dir: Working directory for compilation output (-Mdir).
inc_dirs: List of include directories for `include directives.
Returns:
Path to the compiled executable.
Raises:
RuntimeError: If Verilator is not found or compilation fails.
"""
os.makedirs(work_dir, exist_ok=True)
# Build verilator command
cmd = [self.verilator_path]
cmd.extend(self.compile_args)
cmd.extend(['--top-module', top_module])
# No explicit -std= flag; let Verilator use its default (C++14+)
if inc_dirs:
for d in inc_dirs:
cmd.append(f'+incdir+{d}')
cmd.extend(rtl_files)
cmd.append(tb_cpp)
cmd.extend(['-Mdir', work_dir])
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=120,
cwd=os.getcwd()
)
except subprocess.TimeoutExpired:
raise RuntimeError("Verilator compilation timed out.")
except FileNotFoundError:
raise RuntimeError(
f"Verilator not found at '{self.verilator_path}'."
)
if result.returncode != 0:
tail = result.stderr[-800:] if result.stderr else '(no stderr)'
raise RuntimeError(f"Verilator compilation failed:\n{tail}")
# Executable is at <work_dir>/V<top_module>
executable = os.path.join(work_dir, f'V{top_module}')
if not os.path.exists(executable):
raise RuntimeError(
f"Compilation succeeded but executable not found: {executable}"
)
return executable
def run(self, executable: str, vector_file: str,
timeout_s: int = 30) -> dict:
"""Run the compiled simulation executable.
Args:
executable: Path to the compiled executable.
vector_file: Path to the hex vector file.
timeout_s: Timeout in seconds.
Returns:
dict with keys: success, results, elapsed, stderr.
"""
if not os.path.exists(executable):
return {
'success': False,
'results': [],
'elapsed': 0.0,
'stderr': f'Executable not found: {executable}'
}
start_time = time.time()
try:
result = subprocess.run(
[executable, f'+VECTOR_FILE={vector_file}'],
capture_output=True,
text=True,
timeout=timeout_s
)
elapsed = time.time() - start_time
# Parse stdout for "RESULT: XXX" lines
results = []
for line in result.stdout.splitlines():
line = line.strip()
if line.startswith('RESULT:'):
hex_val = line.split(':', 1)[1].strip()
results.append(hex_val)
success = result.returncode == 0 and len(results) > 0
return {
'success': success,
'results': results,
'elapsed': elapsed,
'stderr': result.stderr
}
except subprocess.TimeoutExpired:
elapsed = time.time() - start_time
return {
'success': False,
'results': [],
'elapsed': elapsed,
'stderr': 'Simulation timed out.'
}