#!/usr/bin/env python3 """SLWChipVerify 一键自动验证脚本。 功能概览: 1) 交互式输入工程目录与仿真参数 2) 自动扫描 Verilog 文件并解析模块关系 3) 自动或手动选择顶层模块 4) 自动生成冒烟测试 testbench 5) 调用 iverilog/vvp 进行编译与仿真 6) 自动输出 VCD 波形 """ from __future__ import annotations import argparse import json import re import shutil import subprocess import sys from dataclasses import dataclass from datetime import datetime from pathlib import Path KEYWORDS = { "module", "endmodule", "if", "else", "case", "endcase", "for", "while", "always", "initial", "assign", "wire", "reg", "logic", "input", "output", "inout", "function", "task", "begin", "end", } LOG_PREFIX = "[SLWChipVerify][one-click]" @dataclass class PortInfo: """端口信息:名称、方向、位宽。""" name: str direction: str width: int @dataclass class ModuleInfo: """模块信息:模块名、来源文件和端口列表。""" name: str file_path: Path ports: list[PortInfo] def _strip_comments(text: str) -> str: """去除 Verilog 的行注释与块注释,便于后续正则解析。""" text = re.sub(r"/\*.*?\*/", "", text, flags=re.S) text = re.sub(r"//.*", "", text) return text def _parse_int_value(expr: str) -> int | None: """把十进制或十六进制数字文本解析为整数。""" value = expr.strip() if re.fullmatch(r"\d+", value): return int(value) if value.lower().startswith("0x"): try: return int(value, 16) except ValueError: return None return None def _parse_range_width(range_text: str | None) -> int: """从位宽范围文本(如 [7:0])计算位宽。""" if not range_text: return 1 m = re.match(r"\[\s*([^:]+)\s*:\s*([^\]]+)\s*\]", range_text) if not m: return 1 msb = _parse_int_value(m.group(1)) lsb = _parse_int_value(m.group(2)) if msb is None or lsb is None: return 1 return abs(msb - lsb) + 1 def _parse_header_ports(header_text: str) -> dict[str, PortInfo]: """解析模块头部端口声明,返回端口名到端口信息的映射。""" parts = [part.strip() for part in header_text.replace("\n", " ").split(",")] names: dict[str, PortInfo] = {} current_direction = "input" current_width = 1 for part in parts: if not part: continue direction_match = re.search(r"\b(input|output|inout)\b", part) width_match = re.search(r"\[[^\]]+\]", part) if direction_match: current_direction = direction_match.group(1).lower() if width_match is None: current_width = 1 if width_match: current_width = _parse_range_width(width_match.group(0)) tokens = re.findall(r"[A-Za-z_][A-Za-z0-9_]*", part) name: str | None = None for token in reversed(tokens): if token.lower() not in { "input", "output", "inout", "wire", "reg", "logic", "signed", "unsigned", }: name = token break if name: names[name] = PortInfo( name=name, direction=current_direction, width=current_width, ) return names def _extract_names_from_decl(decl_text: str) -> list[str]: """从 input/output 声明片段中提取端口名列表。""" cleaned = re.sub(r"\[[^\]]+\]", " ", decl_text) cleaned = re.sub(r"\b(?:reg|wire|logic|signed|unsigned)\b", " ", cleaned) names: list[str] = [] for part in cleaned.split(","): candidate = part.strip() if not candidate: continue candidate = candidate.split("=")[0].strip() if not candidate: continue m = re.match(r"([A-Za-z_][A-Za-z0-9_]*)", candidate) if m: names.append(m.group(1)) return names def _parse_ports_from_body(body_text: str) -> dict[str, PortInfo]: """解析模块体中的端口声明,补全方向与位宽信息。""" port_map: dict[str, PortInfo] = {} decl_re = re.compile(r"^\s*(input|output|inout)\b([^;]*);", re.M) for m in decl_re.finditer(body_text): direction = m.group(1).lower() rest = m.group(2) width_match = re.search(r"\[[^\]]+\]", rest) width = _parse_range_width(width_match.group(0) if width_match else None) names = _extract_names_from_decl(rest) for name in names: port_map[name] = PortInfo(name=name, direction=direction, width=width) return port_map def parse_modules_from_file(file_path: Path) -> list[ModuleInfo]: """从单个 Verilog 文件中提取模块及其端口定义。""" text = file_path.read_text(encoding="utf-8", errors="ignore") text_no_comments = _strip_comments(text) module_re = re.compile( r"\bmodule\s+([A-Za-z_][A-Za-z0-9_]*)\s*" r"(?:#\s*\(.*?\)\s*)?" r"\((.*?)\)\s*;", re.S, ) modules: list[ModuleInfo] = [] pos = 0 while True: m = module_re.search(text_no_comments, pos) if not m: break module_name = m.group(1) header_port_map = _parse_header_ports(m.group(2)) header_ports = list(header_port_map.keys()) body_start = m.end() end_match = re.search(r"\bendmodule\b", text_no_comments[body_start:], re.S) if not end_match: break body_end = body_start + end_match.start() body_text = text_no_comments[body_start:body_end] port_map = _parse_ports_from_body(body_text) ordered_ports: list[PortInfo] = [] seen = set() for name in header_ports: if name in port_map: ordered_ports.append(port_map[name]) elif name in header_port_map: ordered_ports.append(header_port_map[name]) else: ordered_ports.append(PortInfo(name=name, direction="input", width=1)) seen.add(name) for name in sorted(port_map.keys()): if name not in seen: ordered_ports.append(port_map[name]) modules.append(ModuleInfo(name=module_name, file_path=file_path, ports=ordered_ports)) pos = body_end + len("endmodule") return modules def collect_modules(verilog_files: list[Path]) -> list[ModuleInfo]: """汇总多个 Verilog 文件中的全部模块定义。""" modules: list[ModuleInfo] = [] for file_path in verilog_files: modules.extend(parse_modules_from_file(file_path)) return modules def find_instantiated_modules(verilog_files: list[Path], module_names: set[str]) -> set[str]: """扫描实例化关系,找出被其他模块实例化过的模块名。""" instantiated: set[str] = set() for file_path in verilog_files: text = _strip_comments(file_path.read_text(encoding="utf-8", errors="ignore")) for module_name in module_names: pattern = re.compile( rf"\b{re.escape(module_name)}\b\s*(?:#\s*\(.*?\)\s*)?[A-Za-z_][A-Za-z0-9_]*\s*\(", re.S, ) if pattern.search(text): instantiated.add(module_name) return instantiated def _is_clock_name(name: str) -> bool: """判断端口名是否像时钟信号。""" low = name.lower() return low == "clk" or "clock" in low or low.endswith("_clk") def _is_reset_name(name: str) -> bool: """判断端口名是否像复位信号。""" low = name.lower() return ( low == "rst" or low == "reset" or "reset" in low or low.startswith("rst") or low.endswith("_rst") ) def _is_active_low_reset(name: str) -> bool: """根据命名习惯判断复位是否更可能为低有效。""" low = name.lower() return ( low.endswith("_n") or low.endswith("n") or "resetn" in low or "rstn" in low or low.startswith("nreset") or low.startswith("nrst") ) def _sv_width(width: int) -> str: """把位宽整数转换为 SystemVerilog 声明片段。""" if width <= 1: return "" return f"[{width - 1}:0] " def _mask(width: int) -> int: """生成指定位宽的按位掩码。""" if width <= 0: return 0 return (1 << width) - 1 def _format_half_period(period_ns: int) -> str: """将半周期延时格式化为紧凑字符串。""" half = period_ns / 2.0 text = f"{half:.6f}".rstrip("0").rstrip(".") return text or "0" def _is_testbench_module_name(name: str) -> bool: """判断模块名是否呈现 testbench 命名特征。""" low = name.lower() return ( low == "tb" or low.startswith("tb_") or low.endswith("_tb") or "test" in low ) def _top_candidates( modules: list[ModuleInfo], instantiated: set[str], ) -> list[ModuleInfo]: """根据实例化关系和命名特征推断顶层候选模块。""" name_map = {m.name: m for m in modules} roots = sorted([m.name for m in modules if m.name not in instantiated]) if roots: non_tb_roots = [name for name in roots if not _is_testbench_module_name(name)] if non_tb_roots: return [name_map[name] for name in non_tb_roots] non_tb_modules = [m for m in modules if not _is_testbench_module_name(m.name)] if non_tb_modules: max_ports = max(len(m.ports) for m in non_tb_modules) likely_tops = [m for m in non_tb_modules if len(m.ports) == max_ports] return sorted(likely_tops, key=lambda m: m.name) return [name_map[name] for name in roots] non_tb_modules = [m for m in modules if not _is_testbench_module_name(m.name)] if non_tb_modules: max_ports = max(len(m.ports) for m in non_tb_modules) likely_tops = [m for m in non_tb_modules if len(m.ports) == max_ports] return sorted(likely_tops, key=lambda m: m.name) max_ports = max(len(m.ports) for m in modules) likely_tops = [m for m in modules if len(m.ports) == max_ports] return sorted(likely_tops, key=lambda m: m.name) def _pick_top_module( modules: list[ModuleInfo], specified_top: str | None, instantiated: set[str], ) -> ModuleInfo: """确定最终顶层模块:优先使用命令行指定,否则自动推断或交互选择。""" name_map = {m.name: m for m in modules} if specified_top: if specified_top not in name_map: available = ", ".join(sorted(name_map)) raise ChipVerifyError( f"Top module {specified_top!r} not found. Available modules: {available}" ) return name_map[specified_top] candidates = _top_candidates(modules, instantiated) if len(candidates) == 1: return candidates[0] print("Detected multiple top candidates:") for idx, module in enumerate(candidates, start=1): print(f" {idx}. {module.name}") selected = input("Choose top module index (default 1): ").strip() if not selected: return candidates[0] try: index = int(selected) if index < 1 or index > len(candidates): raise ValueError except ValueError as exc: raise ChipVerifyError("Invalid top module selection index") from exc return candidates[index - 1] def generate_testbench( top_module: ModuleInfo, output_dir: Path, cycles: int, period_ns: int, reset_cycles: int, ) -> tuple[Path, Path]: """按顶层模块端口自动生成冒烟测试 testbench 与波形路径。""" tb_name = f"tb_{top_module.name}_auto" tb_path = output_dir / f"{tb_name}.v" vcd_path = output_dir / f"{top_module.name}_auto.vcd" inputs = [p for p in top_module.ports if p.direction == "input"] outputs = [p for p in top_module.ports if p.direction in {"output", "inout"}] clock_port = next((p for p in inputs if p.width == 1 and _is_clock_name(p.name)), None) reset_port = next((p for p in inputs if p.width == 1 and _is_reset_name(p.name)), None) skip_names = { clock_port.name if clock_port else "", reset_port.name if reset_port else "", } non_special_inputs = [p for p in inputs if p.name not in skip_names] lines: list[str] = [] lines.append("`timescale 1ns/1ps") lines.append(f"module {tb_name};") lines.append("") for p in inputs: lines.append(f" reg {_sv_width(p.width)}{p.name};") for p in outputs: lines.append(f" wire {_sv_width(p.width)}{p.name};") lines.append(" integer i;") lines.append(" integer seed;") if not clock_port and non_special_inputs: lines.append(" integer vec;") lines.append("") conn = ", ".join([f".{p.name}({p.name})" for p in top_module.ports]) lines.append(f" {top_module.name} dut ({conn});") if clock_port: half_text = _format_half_period(period_ns) lines.append("") lines.append(" initial begin") lines.append(f" {clock_port.name} = 1'b0;") lines.append(f" forever #{half_text} {clock_port.name} = ~{clock_port.name};") lines.append(" end") lines.append("") lines.append(" initial begin") lines.append(" seed = 20260419;") lines.append(f" $dumpfile(\"{vcd_path.name}\");") lines.append(f" $dumpvars(0, {tb_name});") for p in non_special_inputs: lines.append(f" {p.name} = {p.width}'d0;") if reset_port: active = 0 if _is_active_low_reset(reset_port.name) else 1 inactive = 1 - active lines.append(f" {reset_port.name} = 1'b{active};") if clock_port: if reset_port: lines.append(f" repeat ({reset_cycles}) @(posedge {clock_port.name});") lines.append(f" {reset_port.name} = 1'b{inactive};") lines.append(f" for (i = 0; i < {cycles}; i = i + 1) begin") if non_special_inputs: lines.append(f" @(negedge {clock_port.name});") for p in non_special_inputs: mask = _mask(p.width) lines.append(f" {p.name} = $random(seed) & {p.width}'d{mask};") lines.append(f" @(posedge {clock_port.name});") lines.append(" end") lines.append(" #1;") else: total_width = sum(p.width for p in non_special_inputs) if total_width == 0: lines.append(f" #{max(1, cycles)};") elif total_width <= 8: max_cases = 1 << total_width lines.append(f" for (vec = 0; vec < {max_cases}; vec = vec + 1) begin") bit_offset = 0 for p in non_special_inputs: mask = _mask(p.width) lines.append( f" {p.name} = (vec >> {bit_offset}) & {p.width}'d{mask};" ) bit_offset += p.width lines.append(" #1;") lines.append(" end") else: lines.append(f" for (i = 0; i < {cycles}; i = i + 1) begin") for p in non_special_inputs: mask = _mask(p.width) lines.append(f" {p.name} = $random(seed) & {p.width}'d{mask};") lines.append(" #1;") lines.append(" end") lines.append(" $finish;") lines.append(" end") lines.append("endmodule") tb_path.write_text("\n".join(lines) + "\n", encoding="utf-8") return tb_path, vcd_path def run_simulation( verilog_files: list[Path], tb_path: Path, output_dir: Path, tb_module_name: str, ) -> tuple[Path, str]: """调用 iverilog/vvp 完成编译与仿真,返回可执行文件和仿真输出。""" if shutil.which("iverilog") is None: raise ChipVerifyError("iverilog not found in PATH") if shutil.which("vvp") is None: raise ChipVerifyError("vvp not found in PATH") sim_out = output_dir / "auto_sim.out" include_dirs = sorted({str(p.parent.resolve()) for p in verilog_files}) include_args: list[str] = [] for inc in include_dirs: include_args.extend(["-I", inc]) compile_cmd = [ "iverilog", "-g2012", "-gno-assertions", *include_args, "-s", tb_module_name, "-o", str(sim_out), *[str(p) for p in verilog_files], str(tb_path), ] compile_result = subprocess.run( compile_cmd, capture_output=True, text=True, cwd=output_dir, check=False, ) if compile_result.returncode != 0: message = compile_result.stderr.strip() or compile_result.stdout.strip() raise ChipVerifyError(f"Compile failed:\n{message}") sim_cmd = ["vvp", str(sim_out)] sim_result = subprocess.run( sim_cmd, capture_output=True, text=True, cwd=output_dir, check=False, ) if sim_result.returncode != 0: message = sim_result.stderr.strip() or sim_result.stdout.strip() raise ChipVerifyError(f"Simulation failed:\n{message}") return sim_out, sim_result.stdout def _sanitize_name(value: str) -> str: """把模块名清洗为可用作目录名的安全字符串。""" txt = re.sub(r"[^A-Za-z0-9_.-]", "_", value) txt = txt.strip("._") return txt or "module" def _unique_module_dir(base_dir: Path, module_name: str, used: set[str]) -> Path: """为批量模式生成不冲突的模块输出目录。""" stem = _sanitize_name(module_name) candidate = stem counter = 2 while candidate in used: candidate = f"{stem}_{counter}" counter += 1 used.add(candidate) return base_dir / candidate def run_single_module( top_module: ModuleInfo, verilog_files: list[Path], output_dir: Path, cycles: int, period: int, reset_cycles: int, ) -> dict[str, str | int | None]: """执行单个顶层模块的一键验证流程,并返回结构化结果。""" output_dir.mkdir(parents=True, exist_ok=True) result: dict[str, str | int | None] = { "module": top_module.name, "module_file": str(top_module.file_path), "output_dir": str(output_dir), "status": "fail", "tb_path": None, "sim_binary": None, "sim_log": None, "vcd_path": None, "error": None, } try: tb_path, vcd_path = generate_testbench( top_module=top_module, output_dir=output_dir, cycles=cycles, period_ns=period, reset_cycles=reset_cycles, ) result["tb_path"] = str(tb_path) result["vcd_path"] = str(vcd_path) sim_out, sim_stdout = run_simulation( verilog_files, tb_path, output_dir, tb_module_name=tb_path.stem, ) sim_log = output_dir / "sim_output.log" sim_log.write_text(sim_stdout, encoding="utf-8") result["sim_binary"] = str(sim_out) result["sim_log"] = str(sim_log) result["status"] = "pass" return result except ChipVerifyError as exc: result["error"] = str(exc) return result def write_batch_summary(path: Path, report: dict[str, object]) -> None: """写出批量模式汇总报告。""" path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8") def _prompt_yes_no(prompt: str, default: bool) -> bool: """读取 Y/N 交互输入,并处理默认值。""" default_text = "Y/n" if default else "y/N" raw = input(f"{prompt} [{default_text}]: ").strip().lower() if not raw: return default return raw in {"y", "yes", "1", "true"} def _prompt_with_default(prompt: str, default: str) -> str: """读取字符串输入,若为空则回退默认值。""" user = input(f"{prompt} [{default}]: ").strip() return user or default def _prompt_int(prompt: str, default: int, min_value: int = 0) -> int: """读取整数输入并校验最小值约束。""" raw = _prompt_with_default(prompt, str(default)) try: value = int(raw) except ValueError as exc: raise ChipVerifyError(f"Invalid integer input for {prompt}") from exc if value < min_value: raise ChipVerifyError(f"{prompt} must be >= {min_value}") return value def find_verilog_files(project_dir: Path) -> list[Path]: """递归扫描工程目录中的 .v 源文件。""" files = sorted([p for p in project_dir.rglob("*.v") if p.is_file()]) return files class ChipVerifyError(Exception): """一键验证流程的统一异常类型。""" def main() -> int: """一键流程入口:参数解析、模块选择、仿真执行与结果输出。""" parser = argparse.ArgumentParser( description="One-click Verilog auto testbench + simulation + waveform generator" ) parser.add_argument("--dir", help="Verilog project directory") parser.add_argument("--top", help="Top module name (optional)") parser.add_argument("--out", help="Output directory") parser.add_argument("--cycles", type=int, help="Simulation cycles") parser.add_argument("--period", type=int, help="Clock period (ns)") parser.add_argument("--reset-cycles", type=int, help="Reset hold cycles") parser.add_argument( "--batch", action="store_true", help="Run all detected top candidates and produce a consolidated summary report", ) parser.add_argument( "--batch-report", help="Optional path of consolidated batch summary JSON", ) args = parser.parse_args() print("=== SLWChipVerify One-Click Auto Verification ===") cwd = Path.cwd() project_dir_txt = args.dir or _prompt_with_default("Enter Verilog directory", str(cwd)) project_dir = Path(project_dir_txt).expanduser().resolve() if not project_dir.exists() or not project_dir.is_dir(): raise ChipVerifyError(f"Directory not found: {project_dir}") output_dir = Path( args.out or _prompt_with_default("Output directory", str(project_dir / "slwchipverify_auto")) ).expanduser().resolve() output_dir.mkdir(parents=True, exist_ok=True) cycles = args.cycles if args.cycles is not None else _prompt_int("Simulation cycles", 40, 1) period = args.period if args.period is not None else _prompt_int("Clock period (ns)", 10, 1) reset_cycles = ( args.reset_cycles if args.reset_cycles is not None else _prompt_int("Reset hold cycles", 2, 0) ) verilog_files = find_verilog_files(project_dir) if not verilog_files: raise ChipVerifyError(f"No .v files found in {project_dir}") print(f"Found {len(verilog_files)} Verilog files") modules = collect_modules(verilog_files) if not modules: raise ChipVerifyError("No module declarations found in Verilog files") module_names = {m.name for m in modules} instantiated = find_instantiated_modules(verilog_files, module_names) batch_mode = args.batch if not args.batch and not args.top: candidate_count = len(_top_candidates(modules, instantiated)) if candidate_count > 1: batch_mode = _prompt_yes_no( f"Detected {candidate_count} top candidates. Run in batch mode", True, ) if batch_mode: if args.top: selected_modules = [_pick_top_module(modules, args.top, instantiated)] else: selected_modules = _top_candidates(modules, instantiated) print(f"Batch mode enabled, total candidates: {len(selected_modules)}") for module in selected_modules: print(f" - {module.name} ({module.file_path})") used_dir_names: set[str] = set() batch_results: list[dict[str, str | int | None]] = [] passed = 0 failed = 0 for module in selected_modules: module_output_dir = _unique_module_dir(output_dir, module.name, used_dir_names) if module_output_dir.exists(): shutil.rmtree(module_output_dir) module_output_dir.mkdir(parents=True, exist_ok=True) print(f"\n[batch] Running module: {module.name}") result = run_single_module( top_module=module, verilog_files=verilog_files, output_dir=module_output_dir, cycles=cycles, period=period, reset_cycles=reset_cycles, ) batch_results.append(result) if result["status"] == "pass": passed += 1 print(f"[batch] PASS: {module.name}") print(f"[batch] TB: {result['tb_path']}") print(f"[batch] VCD: {result['vcd_path']}") else: failed += 1 print(f"[batch] FAIL: {module.name}") print(f"[batch] Error: {result['error']}") summary_report = { "timestamp": datetime.now().isoformat(timespec="seconds"), "project_dir": str(project_dir), "output_dir": str(output_dir), "total_candidates": len(selected_modules), "passed": passed, "failed": failed, "results": batch_results, } batch_report_path = ( Path(args.batch_report).expanduser().resolve() if args.batch_report else output_dir / "batch_summary.json" ) write_batch_summary(batch_report_path, summary_report) print("\nBatch run completed") print(f"Batch summary: {batch_report_path}") print(f"Passed: {passed}, Failed: {failed}, Total: {len(selected_modules)}") return 0 if failed == 0 else 1 top_module = _pick_top_module(modules, args.top, instantiated) print(f"Selected top module: {top_module.name}") print(f"Top module file: {top_module.file_path}") tb_path, vcd_path = generate_testbench( top_module=top_module, output_dir=output_dir, cycles=cycles, period_ns=period, reset_cycles=reset_cycles, ) print(f"Generated testbench: {tb_path}") sim_out, sim_stdout = run_simulation( verilog_files, tb_path, output_dir, tb_module_name=tb_path.stem, ) sim_log = output_dir / "sim_output.log" sim_log.write_text(sim_stdout, encoding="utf-8") print("Simulation completed successfully") print(f"Simulation binary: {sim_out}") print(f"Waveform: {vcd_path}") print(f"Simulation log: {sim_log}") print(f"Open waveform with: gtkwave {vcd_path}") return 0 if __name__ == "__main__": try: raise SystemExit(main()) except ChipVerifyError as exc: print(f"{LOG_PREFIX} ERROR: {exc}", file=sys.stderr) raise SystemExit(2) from exc