Init Push
This commit is contained in:
881
one_click_verify.py
Normal file
881
one_click_verify.py
Normal file
@@ -0,0 +1,881 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user