| 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 |
| 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 |
| 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() |
|
|
| |
| for bin_ in result.uncovered_bins: |
| name_lower = bin_.name.lower() |
| gap = CoverageGap(bin_name=bin_.name) |
|
|
| |
| 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) |
|
|
| |
| 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_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) |
|
|
| |
| 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 |
|
|
| |
| 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) |
|
|