semiconductor-pipeline / src /evaluation /coverage_analyzer.py
Sai Kumar Taraka
Initial commit: UVM testbench generator with coverage-driven auto-training
4344b33
from __future__ import annotations
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, List, Optional, Set, Tuple
from src.config import DesignSpec
from src.simulation.base import CoverageBin, SimResult
@dataclass
class CoverageGap:
bin_name: str
register_addr: Optional[int] = None
direction: Optional[str] = None # "read" or "write"
description: str = ""
def to_sequence_hint(self) -> str:
parts = []
if self.direction:
parts.append(f"target_direction={self.direction}")
if self.register_addr is not None:
parts.append(f"target_addr=3'h{self.register_addr:x}")
return ";".join(parts) if parts else ""
@dataclass
class CoverageAnalysis:
sim_result: SimResult
gaps: List[CoverageGap] = field(default_factory=list)
uncovered_registers: Set[int] = field(default_factory=set)
uncovered_directions: Set[str] = field(default_factory=set)
coverage_gain_rate: float = 0.0 # % improvement per iteration
previous_coverage: float = 0.0
@property
def total_gaps(self) -> int:
return len(self.gaps)
def meets_goal(self, threshold: float = 90.0) -> bool:
return self.sim_result.coverage_pct >= threshold
def summary(self) -> str:
r = self.sim_result
return (
f"Coverage: {r.covered_bins}/{r.total_bins} ({r.coverage_pct:.1f}%) | "
f"Gaps: {self.total_gaps} | "
f"Uncovered regs: {sorted(self.uncovered_registers) if self.uncovered_registers else 'none'} | "
f"Rate: {self.coverage_gain_rate:+.1f}%/iter"
)
class CoverageAnalyzer:
"""Analyzes simulation coverage results and identifies gaps for retraining."""
def __init__(self, spec: DesignSpec):
self.spec = spec
self.known_register_addrs = self._extract_register_addrs()
self.history: List[float] = []
def _extract_register_addrs(self) -> Dict[str, int]:
addrs = {}
for reg in self.spec.registers:
try:
addr = int(str(reg.address), 0) if isinstance(reg.address, str) else int(reg.address)
addrs[reg.name.lower()] = addr
except (ValueError, TypeError):
continue
return addrs
def analyze(self, result: SimResult) -> CoverageAnalysis:
self.history.append(result.coverage_pct)
gaps = []
uncovered_regs = set()
uncovered_dirs = set()
# Map bin names to register addresses
for bin_ in result.uncovered_bins:
name_lower = bin_.name.lower()
gap = CoverageGap(bin_name=bin_.name)
# Extract register address from bin name patterns
addr_match = re.search(r'regs\[(\d+)\]', name_lower)
if addr_match:
addr = int(addr_match.group(1))
gap.register_addr = addr
uncovered_regs.add(addr)
# Extract direction
if 'read' in name_lower or 'rd' in name_lower:
gap.direction = 'read'
uncovered_dirs.add('read')
elif 'write' in name_lower or 'wr' in name_lower:
gap.direction = 'write'
uncovered_dirs.add('write')
# cross_ADRxDIR → specific address-direction combos
cross_match = re.search(r'cross.*(\d+).*(read|write)', name_lower)
if cross_match:
addr = int(cross_match.group(1))
direction = cross_match.group(2)
gap.register_addr = addr
gap.direction = direction
uncovered_regs.add(addr)
uncovered_dirs.add(direction)
gap.description = f"Uncovered: {bin_.name} ({bin_.hit_count}/{bin_.goal})"
gaps.append(gap)
# Compute coverage gain rate
gain_rate = 0.0
if len(self.history) >= 2:
recent = self.history[-3:] if len(self.history) >= 3 else self.history
gain_rate = recent[-1] - recent[0]
if len(recent) > 1:
gain_rate /= (len(recent) - 1)
return CoverageAnalysis(
sim_result=result,
gaps=gaps,
uncovered_registers=uncovered_regs,
uncovered_directions=uncovered_dirs,
coverage_gain_rate=gain_rate,
previous_coverage=self.history[-2] if len(self.history) >= 2 else 0.0
)
def generate_target_sequences(self, analysis: CoverageAnalysis) -> List[str]:
"""Generate SystemVerilog sequence code targeting uncovered coverage areas."""
sequences = []
seen = set()
seq_id = len(self.history)
for gap in analysis.gaps:
addr = gap.register_addr
direction = gap.direction
# Deduplicate: (addr, dir) pairs
key = (addr, direction)
if key in seen:
continue
seen.add(key)
seq_name = f"cover_seq_v{seq_id}_a{addr}_{direction or 'any'}"
lines = [f"class {seq_name} extends {self.spec.design_name}_base_seq;",
f" `uvm_object_utils({seq_name})",
"",
f" function new(string name = \"{seq_name}\");",
" super.new(name);",
" endfunction",
"",
" virtual task body();"]
if direction == "write" and addr is not None:
data_val = 0xA0 | addr
lines.append(f" write_reg({addr:0x}, 8'h{data_val:02x});")
lines.append(f' `uvm_info(get_type_name(), "Coverage write reg[{addr:0x}]", UVM_LOW)')
elif direction == "read" and addr is not None:
lines.append(f" read_reg({addr:0x});")
lines.append(f' `uvm_info(get_type_name(), "Coverage read reg[{addr:0x}]", UVM_LOW)')
elif addr is not None:
lines.append(f" write_reg({addr:0x}, 8'hA5);")
lines.append(f" read_reg({addr:0x});")
lines.append(f' `uvm_info(get_type_name(), "Coverage rw reg[{addr:0x}]", UVM_LOW)')
else:
lines.append(" // Generic coverage sequence")
lines.append(' `uvm_info(get_type_name(), "Generic coverage seq", UVM_LOW)')
lines.append(" endtask")
lines.append("endclass")
sequences.append("\n".join(lines))
return sequences
def generate_regression_test(self, seq_names: List[str],
design_name: str) -> str:
"""Generate a regression test that starts all coverage sequences."""
if not seq_names:
seq_names = [f"{design_name}_send_byte_seq",
f"{design_name}_write_reg_seq",
f"{design_name}_read_reg_seq"]
seq_decls = "\n".join(f" {name} seq_{i};"
for i, name in enumerate(seq_names))
seq_starts = "\n".join(
f" seq_{i} = {name}::type_id::create(\"seq_{i}\");\n"
f" seq_{i}.start(env.agent.sequencer);"
for i, name in enumerate(seq_names))
return f"""// Auto-generated regression test — coverage-driven
class {design_name}_regression_test extends {design_name}_test;
`uvm_component_utils({design_name}_regression_test)
function new(string name = "{design_name}_regression_test", uvm_component parent = null);
super.new(name, parent);
endfunction
task run_phase(uvm_phase phase);
super.run_phase(phase);
phase.raise_objection(this);
{seq_decls}
{seq_starts}
phase.drop_objection(this);
endtask
endclass
"""
def target_list_to_sv(self, gaps: List[CoverageGap]) -> str:
"""Generate inline SV assertions/coverage directives for gaps."""
triggers = []
for g in gaps:
cond_parts = []
if g.register_addr is not None:
cond_parts.append(f"wb_addr == 3'h{g.register_addr:x}")
if g.direction == "write":
cond_parts.append("wb_we == 1'b1")
elif g.direction == "read":
cond_parts.append("wb_we == 1'b0")
if cond_parts:
triggers.append(f" cover property(@(posedge vif.clk) "
f"{' && '.join(cond_parts)});")
return "\n".join(triggers)