| from __future__ import annotations |
|
|
| import random |
| import re |
| from pathlib import Path |
| from typing import Any, Dict, List, Optional |
|
|
| from src.simulation.base import CoverageBin, SimResult, Simulator |
|
|
|
|
| PROTOCOL_BUS_SIGNALS = { |
| "axi4lite": {"awvalid", "awready", "awaddr", "wvalid", "wready", "wdata", |
| "bvalid", "bready", "bresp", "arvalid", "arready", "araddr", |
| "rvalid", "rready", "rdata", "rresp"}, |
| "apb": {"psel", "penable", "paddr", "pwrite", "pwdata", "prdata", "pready", "pslverr"}, |
| "wishbone": {"wb_cyc", "wb_stb", "wb_we", "wb_addr", "wb_data_o", "wb_data_i", "wb_ack"}, |
| "uart": {"tx", "rx", "wb_cyc", "wb_stb", "wb_we", "wb_addr", "wb_data_o", "wb_ack"}, |
| "spi": {"mosi", "miso", "sclk", "ss_n", "wb_cyc", "wb_stb", "wb_we", "wb_addr"}, |
| "i2c": {"scl", "sda", "wb_cyc", "wb_stb", "wb_we", "wb_addr"}, |
| } |
|
|
|
|
| class StubSimulator(Simulator): |
| def __init__(self, work_dir: str = "sim_output", seed: int = 42): |
| super().__init__(work_dir) |
| self.rng = random.Random(seed) |
| self.history: List[Dict[str, Any]] = [] |
|
|
| def run(self, files: List[str], top: str = "testbench", |
| plusargs: Optional[List[str]] = None) -> SimResult: |
| |
| seed = 42 |
| if plusargs: |
| for pa in plusargs: |
| if "+seed=" in pa: |
| try: |
| seed = int(pa.split("=")[1]) |
| except (IndexError, ValueError): |
| pass |
| self.rng = random.Random(seed) |
|
|
| all_files = self._collect_all_files(files) |
| bins = self._discover_bins(all_files) |
| covered = self._simulate_coverage(bins, all_files) |
| pct = (sum(1 for b in covered if b.covered) / len(covered) * 100) if covered else 0 |
| return SimResult( |
| passed=pct >= 60, |
| total_bins=len(covered), |
| covered_bins=sum(1 for b in covered if b.covered), |
| bins=covered, |
| errors=[], |
| log_output=self._format_log(covered, pct), |
| seed=seed, |
| ) |
|
|
| def _collect_all_files(self, files: List[str]) -> List[str]: |
| collected = [] |
| seen = set() |
| for f in files: |
| p = Path(f) |
| if not p.exists(): |
| continue |
| resolved = str(p.resolve()) |
| if resolved in seen: |
| continue |
| seen.add(resolved) |
| collected.append(resolved) |
| |
| for subdir in p.parent.glob("**/*"): |
| if subdir.is_file() and subdir.suffix in (".sv", ".v", ".vh"): |
| r = str(subdir.resolve()) |
| if r not in seen: |
| seen.add(r) |
| collected.append(r) |
| return collected |
|
|
| def _discover_bins(self, files: List[str]) -> List[CoverageBin]: |
| bins_dict: Dict[str, CoverageBin] = {} |
| all_text = "\n".join( |
| Path(f).read_text(errors="replace") for f in files if Path(f).exists() |
| ) |
|
|
| |
| for m in re.finditer(r'coverpoint\s+(\w+)\s*\{([^}]+)\}', all_text): |
| cp_name = m.group(1) |
| body = m.group(2) |
| for bm in re.finditer(r'bins\s+(\w+)\s*=', body): |
| key = f"{cp_name}.{bm.group(1)}" |
| if key not in bins_dict: |
| bins_dict[key] = CoverageBin(name=key, hit_count=0, goal=1) |
|
|
| |
| for m in re.finditer(r'cross\s+(\w+)\s*,\s*(\w+)\s*\{', all_text): |
| key = f"cross_{m.group(1)}x{m.group(2)}" |
| if key not in bins_dict: |
| bins_dict[key] = CoverageBin(name=key, hit_count=0, goal=1) |
|
|
| |
| for m in re.finditer(r'cover property\s*\([^)]*\)', all_text): |
| key = f"sva_cover_{len(bins_dict)}" |
| if key not in bins_dict: |
| bins_dict[key] = CoverageBin(name=key, hit_count=0, goal=1) |
|
|
| if not bins_dict: |
| for addr in range(8): |
| bins_dict[f"bus_cg.ADDR.regs[{addr}]"] = CoverageBin( |
| f"bus_cg.ADDR.regs[{addr}]", 0, 1) |
| bins_dict[f"cross_ADRxDIR.addr{addr}_read"] = CoverageBin( |
| f"cross_ADRxDIR.addr{addr}_read", 0, 1) |
| bins_dict[f"cross_ADRxDIR.addr{addr}_write"] = CoverageBin( |
| f"cross_ADRxDIR.addr{addr}_write", 0, 1) |
| bins_dict["bus_cg.DIR.read"] = CoverageBin("bus_cg.DIR.read", 0, 1) |
| bins_dict["bus_cg.DIR.write"] = CoverageBin("bus_cg.DIR.write", 0, 1) |
|
|
| return list(bins_dict.values()) |
|
|
| def _is_register_hit(self, addr: int, all_text: str) -> bool: |
| patterns = [ |
| f"reg_addr = 3'h{addr:x}", |
| f"reg_addr=3'h{addr:x}", |
| f"wb_addr = 3'h{addr:x}", |
| f"addr={addr}", |
| f"target_addr=3'h{addr:x}", |
| ] |
| return any(p in all_text for p in patterns) |
|
|
| def _simulate_coverage(self, bins: List[CoverageBin], |
| files: List[str]) -> List[CoverageBin]: |
| if not files: |
| return bins |
|
|
| all_text = "\n".join( |
| Path(f).read_text(errors="replace") for f in files if Path(f).exists() |
| ) |
|
|
| has_write = "item.we = 1" in all_text or "wb_we = 1" in all_text or "pwrite = 1" in all_text |
| has_read = "item.we = 0" in all_text or "wb_we = 0" in all_text or "pwrite = 0" in all_text |
| has_any_txn = bool(re.search(r'reg_addr\s*=|wb_addr\s*=|paddr\s*=', all_text)) |
|
|
| result = [] |
| for b in bins: |
| nl = b.name.lower() |
| hit = 0 |
| goal = b.goal |
|
|
| if "addr" in nl or "regs[" in nl: |
| for a in range(8): |
| if self._is_register_hit(a, all_text): |
| if f"[{a}]" in b.name or f"regs[{a}]" in nl: |
| hit += 1 |
| if f"addr{a}" in nl: |
| hit += 1 |
| if hit == 0 and has_any_txn: |
| for a in range(8): |
| if f"regs[{a}]" in nl and any( |
| f"for (int a = {a}" in all_text or f"a == {a}" in all_text |
| or f"a={a}" in all_text |
| for _ in [0] |
| ): |
| hit += 1 |
|
|
| elif "read" in nl and ("dir" in nl or "direction" in nl): |
| hit = 1 if has_read else 0 |
| elif "write" in nl and ("dir" in nl or "direction" in nl): |
| hit = 1 if has_write else 0 |
|
|
| elif "cross" in nl: |
| if has_write and has_read: |
| hit = sum(1 for a in range(8) if self._is_register_hit(a, all_text)) |
| hit = min(hit, 8) |
| elif has_write or has_read: |
| hit = sum(1 for a in range(8) if self._is_register_hit(a, all_text)) |
|
|
| elif "zero" in nl: |
| hit = 1 if "8'h00" in all_text else 0 |
| elif "ones" in nl: |
| hit = 1 if "8'hFF" in all_text else 0 |
| elif "pattern" in nl or "data" in nl: |
| hit = 1 if re.search(r"8'h[0-9A-Fa-f]{2}", all_text) else 0 |
|
|
| elif "sva" in nl: |
| hit = 1 if "assert property" in all_text or "cover property" in all_text else 0 |
|
|
| elif "serial" in nl or "uart" in nl or "tx" in nl: |
| tx_present = "uart_tx" in all_text or ".tx" in all_text |
| rx_present = "uart_rx" in all_text or ".rx" in all_text |
| hit = (1 if tx_present else 0) + (1 if rx_present else 0) |
| goal = 2 |
|
|
| elif "spi" in nl or "sclk" in nl or "mosi" in nl: |
| hit = 1 if any(s in all_text for s in ["mosi", "miso", "sclk", "ss_n"]) else 0 |
|
|
| elif "i2c" in nl or "scl" in nl or "sda" in nl: |
| hit = 1 if "scl" in all_text or "sda" in all_text else 0 |
|
|
| else: |
| hit = 1 if has_any_txn else 0 |
| goal = 1 |
|
|
| result.append(CoverageBin(name=b.name, hit_count=hit, goal=goal)) |
|
|
| return result |
|
|
| def _format_log(self, bins: List[CoverageBin], pct: float) -> str: |
| lines = [ |
| "--- Simulation Start ---", |
| "UVM_INFO @ 0: reporter [RNTST] Running test ...", |
| ] |
| for b in bins: |
| status = "HIT" if b.covered else "MISS" |
| lines.append(f"COVERAGE: {b.name} {b.hit_count}/{b.goal} [{status}]") |
| covered = sum(1 for b in bins if b.covered) |
| total = len(bins) |
| lines.append(f"--- Coverage: {covered}/{total} ({pct:.1f}%) ---") |
| if covered == total: |
| lines.append("SCOREBOARD: PASS") |
| lines.append("UVM Report Summary: Errors: 0") |
| else: |
| lines.append("SCOREBOARD: PARTIAL") |
| lines.append(f"UVM Report Summary: Warnings: {total - covered}") |
| return "\n".join(lines) |
|
|
| def name(self) -> str: |
| return "StubSimulator" |
|
|