diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3b649aa0..7b125aaf 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v0.5.0 ## 📑 Table of Contents +- [Version 0.13.39 (2025-10-19)](#version-01339-2025-10-19) +- [Version 0.13.38 (2025-10-19)](#version-01338-2025-10-19) - [Version 0.13.37 (2025-09-30)](#version-01337-2025-09-30) - [Version 0.13.36 (2025-09-29)](#version-01336-2025-09-29) - [Version 0.13.35 (2025-09-26)](#version-01335-2025-09-26) @@ -49,6 +51,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v0.5.0 - [Version 0.13.4 (2025-08-16)](#version-0134-2025-08-16) - [Version 0.5.8 (2025-06-09)](#020---2025-06-09) +--- +## Version 0.13.39 (2025-10-19) + +Minor updates and improvements. + +--- +## Version 0.13.38 (2025-10-19) + +Minor updates and improvements. + --- ## Version 0.13.37 (2025-09-30) diff --git a/src/__version__.py b/src/__version__.py index 0be90887..ca237f05 100644 --- a/src/__version__.py +++ b/src/__version__.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 """Version information for PCILeech Firmware Generator.""" -__version__ = "0.13.37" -__version_info__ = (0, 13, 37) +__version__ = "0.13.39" +__version_info__ = (0, 13, 39) # Release information __title__ = "PCILeech Firmware Generator" @@ -13,5 +13,5 @@ __url__ = "https://github.com/voltcyclone/PCILeechFWGenerator" # Build metadata -__build_date__ = "2025-10-01T03:02:54.782549" -__commit_hash__ = "2cefaae" +__build_date__ = "2025-10-19T06:30:42.327374" +__commit_hash__ = "d023f3d" diff --git a/src/behavioral/__init__.py b/src/behavioral/__init__.py new file mode 100644 index 00000000..54a3b6ae --- /dev/null +++ b/src/behavioral/__init__.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +"""Behavioral device simulation for PCILeech firmware generation. + +This module provides behavioral simulation capabilities that make generated +firmware behave like real devices (e.g., Ethernet showing as connected, +NVMe showing as ready). +""" + +from .base import ( + BehaviorType, + BehavioralRegister, + BehavioralCounter, + BehavioralSpec +) +from .analyzer import ( + BehavioralAnalyzerFactory, + generate_behavioral_spec +) + +__all__ = [ + 'BehaviorType', + 'BehavioralRegister', + 'BehavioralCounter', + 'BehavioralSpec', + 'BehavioralAnalyzerFactory', + 'generate_behavioral_spec', +] diff --git a/src/behavioral/analyzer.py b/src/behavioral/analyzer.py new file mode 100644 index 00000000..036871a9 --- /dev/null +++ b/src/behavioral/analyzer.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +"""Behavioral analyzer factory and dispatcher.""" + +import logging +from typing import Any, Optional + +from string_utils import log_info_safe, log_warning_safe, safe_format +from .base import BehavioralSpec +from .network_behavioral import NetworkBehavioralAnalyzer +from .storage_behavioral import StorageBehavioralAnalyzer +from .media_behavioral import MediaBehavioralAnalyzer + + +logger = logging.getLogger(__name__) + + +class BehavioralAnalyzerFactory: + """Factory for creating device-specific behavioral analyzers.""" + + @staticmethod + def create_analyzer(device_config: Any) -> Optional[Any]: + """Create appropriate behavioral analyzer based on device class.""" + class_code = getattr(device_config, 'class_code', 0) + device_class = (class_code >> 16) & 0xFF + + log_info_safe(logger, safe_format("Creating behavioral analyzer for class=0x{cls:02X}", + cls=device_class)) + + if device_class == 0x02: # Network controller + return NetworkBehavioralAnalyzer(device_config) + elif device_class == 0x01: # Storage controller + return StorageBehavioralAnalyzer(device_config) + elif device_class == 0x04: # Multimedia controller + return MediaBehavioralAnalyzer(device_config) + else: + log_warning_safe(logger, safe_format("No behavioral analyzer for class=0x{cls:02X}", + cls=device_class)) + return None + + @staticmethod + def generate_behavioral_spec(device_config: Any) -> Optional[BehavioralSpec]: + """Generate behavioral specification for device.""" + if not getattr(device_config, 'enable_behavioral_simulation', False): + log_info_safe(logger, "Behavioral simulation disabled") + return None + + analyzer = BehavioralAnalyzerFactory.create_analyzer(device_config) + if not analyzer: + return None + + return analyzer.generate_spec() + + +def generate_behavioral_spec(device_config: Any) -> Optional[BehavioralSpec]: + """Convenience function to generate behavioral spec.""" + return BehavioralAnalyzerFactory.generate_behavioral_spec(device_config) diff --git a/src/behavioral/base.py b/src/behavioral/base.py new file mode 100644 index 00000000..d5e3d2b9 --- /dev/null +++ b/src/behavioral/base.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +"""Base behavioral register infrastructure for device simulation.""" + +import logging +from typing import Dict, Any, List, Optional +from dataclasses import dataclass +from enum import Enum + +from string_utils import log_error_safe, log_debug_safe, safe_format + + +logger = logging.getLogger(__name__) + + +class BehaviorType(Enum): + """Register behavior types for simulation.""" + CONSTANT = "constant" # Always returns fixed value + AUTO_INCREMENT = "auto_increment" # Auto-incrementing counter + WRITE_CAPTURE = "write_capture" # Captures written value + RANDOM = "random" # Random data generation + PATTERN = "pattern" # Pattern-based generation + TRIGGERED = "triggered" # State change on trigger + PERIODIC = "periodic" # Periodic value changes + + +@dataclass +class BehavioralRegister: + """Definition of a behavioral register.""" + name: str + offset: int + behavior: BehaviorType + default_value: int = 0x00000000 + pattern: Optional[str] = None + counter_bits: Optional[int] = None + description: str = "" + read_only: bool = False + + def to_dict(self) -> Dict[str, Any]: + """Convert to template-compatible dictionary.""" + result = { + "offset": self.offset, + "behavior": self.behavior.value, + "default": self.default_value, + "description": self.description, + "read_only": self.read_only + } + if self.pattern: + result["pattern"] = self.pattern + if self.counter_bits: + result["counter_bits"] = self.counter_bits + return result + + +@dataclass +class BehavioralCounter: + """Definition of a behavioral counter.""" + name: str + width: int + increment_rate: int = 1 + reset_value: int = 0 + description: str = "" + + def to_dict(self) -> Dict[str, Any]: + """Convert to template-compatible dictionary.""" + return { + "width": self.width, + "increment_rate": self.increment_rate, + "reset_value": self.reset_value, + "description": self.description + } + + +class BehavioralSpec: + """Complete behavioral specification for a device.""" + + def __init__(self, device_category: str): + self.device_category = device_category + self.registers: Dict[str, BehavioralRegister] = {} + self.counters: Dict[str, BehavioralCounter] = {} + self.state_machines: Dict[str, Any] = {} + + def add_register(self, register: BehavioralRegister) -> None: + """Add a behavioral register.""" + log_debug_safe(logger, safe_format("Adding behavioral register: {name} at 0x{offset:04X}", + name=register.name, offset=register.offset)) + self.registers[register.name] = register + + def add_counter(self, counter: BehavioralCounter) -> None: + """Add a behavioral counter.""" + log_debug_safe(logger, safe_format("Adding counter: {name} ({width} bits)", + name=counter.name, width=counter.width)) + self.counters[counter.name] = counter + + def to_dict(self) -> Dict[str, Any]: + """Convert to template-compatible dictionary.""" + return { + "device_category": self.device_category, + "registers": {k: v.to_dict() for k, v in self.registers.items()}, + "counters": {k: v.to_dict() for k, v in self.counters.items()}, + "state_machines": self.state_machines + } + + def validate(self) -> bool: + """Validate the behavioral specification.""" + # Check for offset conflicts + offsets = {} + for name, reg in self.registers.items(): + if reg.offset in offsets: + log_error_safe(logger, safe_format("Offset conflict: {name1} and {name2} at 0x{offset:04X}", + name1=offsets[reg.offset], name2=name, offset=reg.offset)) + return False + offsets[reg.offset] = name + + # Validate counter references in patterns + for name, reg in self.registers.items(): + if reg.pattern and reg.behavior == BehaviorType.AUTO_INCREMENT: + # Check if pattern references valid counters + for counter_name in self.counters.keys(): + if counter_name in reg.pattern: + log_debug_safe(logger, safe_format("Register {reg} uses counter {cnt}", + reg=name, cnt=counter_name)) + + return True + + +def require(condition: bool, message: str, **context) -> None: + """Validate condition or exit with error.""" + if not condition: + log_error_safe(safe_format("Build aborted: {msg} | ctx={ctx}", + msg=message, ctx=context)) + raise SystemExit(2) diff --git a/src/behavioral/media_behavioral.py b/src/behavioral/media_behavioral.py new file mode 100644 index 00000000..811fbec2 --- /dev/null +++ b/src/behavioral/media_behavioral.py @@ -0,0 +1,257 @@ +#!/usr/bin/env python3 +"""Behavioral simulation for media controllers (audio/video).""" + +import logging +from typing import Dict, Any, Optional + +from string_utils import log_info_safe, safe_format +from .base import ( + BehavioralSpec, + BehavioralRegister, + BehavioralCounter, + BehaviorType +) + + +logger = logging.getLogger(__name__) + + +class MediaBehavioralAnalyzer: + """Generate behavioral specs for media controllers.""" + + # Audio/Video controller registers + MEDIA_REGISTERS = { + # Codec status and control + "codec_status": { + "offset": 0x0000, + "behavior": BehaviorType.CONSTANT, + "value": 0x00000003, # Codec ready + initialized + "description": "Codec status (bits: 0=ready, 1=initialized)" + }, + "codec_version": { + "offset": 0x0004, + "behavior": BehaviorType.CONSTANT, + "value": 0x00010203, # Version 1.2.3 + "description": "Codec version register" + }, + + # Audio stream registers + "audio_stream_status": { + "offset": 0x0010, + "behavior": BehaviorType.CONSTANT, + "value": 0x00000001, # Stream ready + "description": "Audio stream status" + }, + "audio_buffer_position": { + "offset": 0x0014, + "behavior": BehaviorType.AUTO_INCREMENT, + "pattern": "audio_position_counter[15:0]", + "counter_bits": 16, + "description": "Audio buffer position pointer" + }, + "audio_sample_rate": { + "offset": 0x0018, + "behavior": BehaviorType.CONSTANT, + "value": 0x0000AC44, # 44.1 kHz + "description": "Audio sample rate (Hz)" + }, + "audio_channels": { + "offset": 0x001C, + "behavior": BehaviorType.CONSTANT, + "value": 0x00000002, # Stereo + "description": "Number of audio channels" + }, + + # Video stream registers + "video_stream_status": { + "offset": 0x0020, + "behavior": BehaviorType.CONSTANT, + "value": 0x00000001, # Stream ready + "description": "Video stream status" + }, + "video_frame_counter": { + "offset": 0x0024, + "behavior": BehaviorType.AUTO_INCREMENT, + "pattern": "video_frame_counter", + "description": "Video frame counter" + }, + "video_resolution": { + "offset": 0x0028, + "behavior": BehaviorType.CONSTANT, + "value": 0x07800438, # 1920x1080 + "description": "Video resolution (width<<16 | height)" + }, + "video_fps": { + "offset": 0x002C, + "behavior": BehaviorType.CONSTANT, + "value": 0x0000003C, # 60 FPS + "description": "Video frames per second" + }, + + # DMA and buffer management + "dma_status": { + "offset": 0x0030, + "behavior": BehaviorType.CONSTANT, + "value": 0x00000001, # DMA ready + "description": "DMA engine status" + }, + "dma_buffer_count": { + "offset": 0x0034, + "behavior": BehaviorType.AUTO_INCREMENT, + "pattern": "dma_buffer_counter[7:0]", + "counter_bits": 8, + "description": "DMA buffer completion counter" + }, + "buffer_level": { + "offset": 0x0038, + "behavior": BehaviorType.AUTO_INCREMENT, + "pattern": "32'h0000 | buffer_level_counter[15:0]", + "counter_bits": 16, + "description": "Current buffer fill level" + }, + + # Interrupt and event registers + "interrupt_status": { + "offset": 0x0040, + "behavior": BehaviorType.WRITE_CAPTURE, + "default": 0x00000000, + "description": "Interrupt status (write 1 to clear)" + }, + "interrupt_enable": { + "offset": 0x0044, + "behavior": BehaviorType.WRITE_CAPTURE, + "default": 0x00000000, + "description": "Interrupt enable mask" + }, + + # Performance monitoring + "frames_processed": { + "offset": 0x0050, + "behavior": BehaviorType.AUTO_INCREMENT, + "pattern": "frames_processed_counter", + "description": "Total frames processed" + }, + "bytes_transferred": { + "offset": 0x0054, + "behavior": BehaviorType.AUTO_INCREMENT, + "pattern": "bytes_transferred_counter", + "description": "Total bytes transferred" + }, + + # Codec configuration (writable) + "codec_control": { + "offset": 0x0060, + "behavior": BehaviorType.WRITE_CAPTURE, + "default": 0x00000001, # Default: enabled + "description": "Codec control register" + }, + "volume_control": { + "offset": 0x0064, + "behavior": BehaviorType.WRITE_CAPTURE, + "default": 0x00008080, # 50% volume for L/R + "description": "Volume control (left<<8 | right)" + }, + } + + def __init__(self, device_config: Any): + self._device_config = device_config + self._subclass = getattr(device_config, 'subclass_code', 0) + + def _is_audio_device(self) -> bool: + """Check if device is an audio controller.""" + # PCI class 0x04 subclass 0x01 (multimedia audio controller) + return (self._subclass & 0xFF) == 0x01 + + def _is_video_device(self) -> bool: + """Check if device is a video controller.""" + # PCI class 0x04 subclass 0x00 (multimedia video controller) + return (self._subclass & 0xFF) == 0x00 + + def generate_spec(self) -> Optional[BehavioralSpec]: + """Generate behavioral specification for media device.""" + log_info_safe(logger, safe_format("Generating media behavioral spec for device={dev}", + dev=getattr(self._device_config, 'device_id', 'unknown'))) + + # Determine device category + if self._is_audio_device(): + category = "audio" + elif self._is_video_device(): + category = "video" + else: + category = "media" # Generic media + + spec = BehavioralSpec(category) + + # Add all media registers + for name, reg_def in self.MEDIA_REGISTERS.items(): + # Skip video-specific registers for audio-only devices + if self._is_audio_device() and not self._is_video_device(): + if name.startswith("video_"): + continue + + # Skip audio-specific registers for video-only devices + if self._is_video_device() and not self._is_audio_device(): + if name.startswith("audio_"): + continue + + register = BehavioralRegister( + name=name, + offset=reg_def["offset"], + behavior=reg_def["behavior"], + default_value=reg_def.get("value", reg_def.get("default", 0)), + pattern=reg_def.get("pattern"), + counter_bits=reg_def.get("counter_bits"), + description=reg_def["description"] + ) + spec.add_register(register) + + # Add counters + spec.add_counter(BehavioralCounter( + name="audio_position_counter", + width=16, + increment_rate=64, # Increment by sample block size + description="Audio buffer position counter" + )) + + spec.add_counter(BehavioralCounter( + name="video_frame_counter", + width=32, + increment_rate=1, + description="Video frame counter" + )) + + spec.add_counter(BehavioralCounter( + name="dma_buffer_counter", + width=8, + increment_rate=1, + description="DMA buffer completion counter" + )) + + spec.add_counter(BehavioralCounter( + name="buffer_level_counter", + width=16, + increment_rate=128, # Buffer fill increment + description="Buffer level counter" + )) + + spec.add_counter(BehavioralCounter( + name="frames_processed_counter", + width=32, + increment_rate=1, + description="Total frames processed" + )) + + spec.add_counter(BehavioralCounter( + name="bytes_transferred_counter", + width=32, + increment_rate=1024, # 1KB per cycle + description="Bytes transferred counter" + )) + + # Validate and return + if not spec.validate(): + from string_utils import log_error_safe + log_error_safe(logger, "Failed to validate media behavioral spec") + return None + + return spec diff --git a/src/behavioral/network_behavioral.py b/src/behavioral/network_behavioral.py new file mode 100644 index 00000000..984dfafb --- /dev/null +++ b/src/behavioral/network_behavioral.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""Behavioral simulation for network controllers.""" + +import logging +from typing import Dict, Any, Optional + +from string_utils import log_info_safe, safe_format +from .base import ( + BehavioralSpec, + BehavioralRegister, + BehavioralCounter, + BehaviorType +) + + +logger = logging.getLogger(__name__) + + +class NetworkBehavioralAnalyzer: + """Generate behavioral specs for network controllers.""" + + ETHERNET_REGISTERS = { + # Standard Ethernet controller registers + "link_status": { + "offset": 0x0000, + "behavior": BehaviorType.CONSTANT, + "value": 0x00000001, # Link always up + "description": "Link status (bit 0: link up)" + }, + "rx_data": { + "offset": 0x0004, + "behavior": BehaviorType.AUTO_INCREMENT, + "pattern": "32'hAABB0000 | rx_counter[15:0]", + "counter_bits": 16, + "description": "Simulated RX data" + }, + "tx_data": { + "offset": 0x0008, + "behavior": BehaviorType.WRITE_CAPTURE, + "default": 0x00000000, + "description": "TX data capture" + }, + "mac_addr_low": { + "offset": 0x0010, + "behavior": BehaviorType.CONSTANT, + "value": 0x12345678, + "description": "MAC address low 32 bits" + }, + "mac_addr_high": { + "offset": 0x0014, + "behavior": BehaviorType.CONSTANT, + "value": 0x00009ABC, + "description": "MAC address high 16 bits" + }, + "rx_packet_count": { + "offset": 0x0020, + "behavior": BehaviorType.AUTO_INCREMENT, + "pattern": "rx_packet_counter", + "description": "RX packet counter" + }, + "tx_packet_count": { + "offset": 0x0024, + "behavior": BehaviorType.AUTO_INCREMENT, + "pattern": "tx_packet_counter", + "description": "TX packet counter" + } + } + + def __init__(self, device_config: Any): + self._device_config = device_config + self._subclass = getattr(device_config, 'subclass_code', 0) + + def generate_spec(self) -> Optional[BehavioralSpec]: + """Generate behavioral specification for network device.""" + log_info_safe(logger, safe_format("Generating network behavioral spec for device={dev}", + dev=getattr(self._device_config, 'device_id', 'unknown'))) + + spec = BehavioralSpec("ethernet") + + # Add standard Ethernet registers + for name, reg_def in self.ETHERNET_REGISTERS.items(): + register = BehavioralRegister( + name=name, + offset=reg_def["offset"], + behavior=reg_def["behavior"], + default_value=reg_def.get("value", reg_def.get("default", 0)), + pattern=reg_def.get("pattern"), + counter_bits=reg_def.get("counter_bits"), + description=reg_def["description"] + ) + spec.add_register(register) + + # Add counters + spec.add_counter(BehavioralCounter( + name="rx_counter", + width=32, + increment_rate=1, + description="RX data counter" + )) + + spec.add_counter(BehavioralCounter( + name="rx_packet_counter", + width=32, + increment_rate=1, + description="RX packet counter" + )) + + spec.add_counter(BehavioralCounter( + name="tx_packet_counter", + width=32, + increment_rate=1, + description="TX packet counter" + )) + + # Validate and return + if not spec.validate(): + from string_utils import log_error_safe + log_error_safe(logger, "Failed to validate network behavioral spec") + return None + + return spec diff --git a/src/behavioral/storage_behavioral.py b/src/behavioral/storage_behavioral.py new file mode 100644 index 00000000..c2d5e284 --- /dev/null +++ b/src/behavioral/storage_behavioral.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +"""Behavioral simulation for storage controllers.""" + +import logging +from typing import Dict, Any, Optional + +from string_utils import log_info_safe, safe_format +from .base import ( + BehavioralSpec, + BehavioralRegister, + BehavioralCounter, + BehaviorType +) + + +logger = logging.getLogger(__name__) + + +class StorageBehavioralAnalyzer: + """Generate behavioral specs for storage controllers.""" + + NVME_REGISTERS = { + "controller_status": { + "offset": 0x001C, + "behavior": BehaviorType.CONSTANT, + "value": 0x00000001, # Controller ready + "description": "Controller status (CSTS)" + }, + "admin_queue_attrs": { + "offset": 0x0024, + "behavior": BehaviorType.CONSTANT, + "value": 0x00FF00FF, # Queue sizes + "description": "Admin queue attributes" + }, + "completion_queue_head": { + "offset": 0x1000, + "behavior": BehaviorType.AUTO_INCREMENT, + "pattern": "cq_head_counter[15:0]", + "counter_bits": 16, + "description": "Completion queue head pointer" + }, + "submission_queue_tail": { + "offset": 0x1004, + "behavior": BehaviorType.WRITE_CAPTURE, + "default": 0x00000000, + "description": "Submission queue tail pointer" + } + } + + def __init__(self, device_config: Any): + self._device_config = device_config + + def generate_spec(self) -> Optional[BehavioralSpec]: + """Generate behavioral specification for storage device.""" + log_info_safe(logger, safe_format("Generating storage behavioral spec for device={dev}", + dev=getattr(self._device_config, 'device_id', 'unknown'))) + + spec = BehavioralSpec("nvme") + + # Add NVMe registers + for name, reg_def in self.NVME_REGISTERS.items(): + register = BehavioralRegister( + name=name, + offset=reg_def["offset"], + behavior=reg_def["behavior"], + default_value=reg_def.get("value", reg_def.get("default", 0)), + pattern=reg_def.get("pattern"), + counter_bits=reg_def.get("counter_bits"), + description=reg_def["description"] + ) + spec.add_register(register) + + # Add counters + spec.add_counter(BehavioralCounter( + name="cq_head_counter", + width=16, + increment_rate=1, + description="Completion queue head counter" + )) + + # Validate and return + if not spec.validate(): + from string_utils import log_error_safe + log_error_safe(logger, "Failed to validate storage behavioral spec") + return None + + return spec diff --git a/src/build.py b/src/build.py index 3f89c17c..6a688de7 100644 --- a/src/build.py +++ b/src/build.py @@ -20,7 +20,7 @@ from concurrent.futures import ThreadPoolExecutor, as_completed from dataclasses import dataclass, field from pathlib import Path -from typing import Any, Callable, Dict, List, Optional, Protocol, Tuple, Union +from typing import Any, Callable, Dict, List, Optional, Tuple, Union from src.string_utils import ( log_debug_safe, @@ -30,7 +30,6 @@ safe_format, ) from src.templating.template_context_validator import clear_global_template_cache -from src.utils.log_phases import PhaseLogger # Import board functions from the correct module from .device_clone.constants import PRODUCTION_DEFAULTS @@ -70,14 +69,9 @@ SYSTEMVERILOG_EXTENSION = ".sv" # ────────────────────────────────────────────────────────────────────────────── -# Type Definitions and Protocols +# Type Definitions # ────────────────────────────────────────────────────────────────────────────── - -# Local lightweight helpers to deduplicate recurring hex/int normalization -# patterns within this module only (not part of public API surface). - - def _as_int(value: Union[int, str], field: str) -> int: """Normalize numeric identifier that may be int or hex string.""" if isinstance(value, int): @@ -143,14 +137,6 @@ class DeviceConfiguration: pcie_lanes: int -class FileWriter(Protocol): - """Protocol for file writing implementations.""" - - def write_file(self, path: Path, content: str) -> None: - """Write content to a file.""" - ... - - # ────────────────────────────────────────────────────────────────────────────── # Module Import Checker # ────────────────────────────────────────────────────────────────────────────── @@ -938,7 +924,6 @@ def __init__( # Core configuration & logger self.config = config self.logger = logger or get_logger(self.__class__.__name__) - self._phases = PhaseLogger(self.logger) # Initialize managers (dependency injection with defaults) self.msix_manager = msix_manager or MSIXManager(config.bdf, self.logger) @@ -957,8 +942,12 @@ def __init__( self._device_config: Optional[DeviceConfiguration] = None def _phase(self, message: str) -> None: - """Backward-compatible shim for previous phase logging.""" - self._phases.begin(message) + """Log a build phase message with standardized formatting.""" + log_info_safe( + self.logger, + safe_format("➤ {msg}", msg=message), + prefix="BUILD", + ) def build(self) -> List[str]: """ diff --git a/src/cli/cli.py b/src/cli/cli.py index 7d5ee2e8..f75c8115 100644 --- a/src/cli/cli.py +++ b/src/cli/cli.py @@ -280,11 +280,13 @@ def main(argv: Optional[List[str]] = None): if update_available: prompt_for_update(latest_version) else: + from ..__version__ import __version__ + log_info_safe( logger, safe_format( - "✓ You are running the latest version ({latest_version})", - latest_version=latest_version, + "✓ You are running the latest version ({current_version})", + current_version=__version__, ), prefix="VERS", ) diff --git a/src/device_clone/fallback_manager.py b/src/device_clone/fallback_manager.py index 92973012..e228dcc0 100644 --- a/src/device_clone/fallback_manager.py +++ b/src/device_clone/fallback_manager.py @@ -112,6 +112,12 @@ class FallbackManager: "board.fpga_family": "7series", "board.pcie_ip_type": "pcie7x", "sys_clk_freq_mhz": 100, + # PCIe clock parameters for Xilinx 7-series + "pcie_refclk_freq": 0, # 0=100MHz, 1=125MHz, 2=250MHz + "pcie_userclk1_freq": 2, # 1=31.25MHz, 2=62.5MHz, 3=125MHz, 4=250MHz, 5=500MHz + "pcie_userclk2_freq": 2, # Same encoding as userclk1 + "pcie_link_speed": 2, # 1=Gen1, 2=Gen2, 3=Gen3 + "pcie_oobclk_mode": 1, # OOB clock mode "generated_xdc_path": "", "board_xdc_content": "", "max_lanes": 1, diff --git a/src/templates/sv/behavioral/pcileech_bar_behavioral.sv.j2 b/src/templates/sv/behavioral/pcileech_bar_behavioral.sv.j2 new file mode 100644 index 00000000..4d2f4fb2 --- /dev/null +++ b/src/templates/sv/behavioral/pcileech_bar_behavioral.sv.j2 @@ -0,0 +1,123 @@ +{#- Behavioral Device Simulation Module Template -#} +{{ header | safe }} + +`default_nettype none + +module pcileech_bar_impl_{{ behavioral_spec.device_category }}_behavioral #( + parameter BAR_INDEX = {{ bar_index | default(0) }} +) ( + input wire rst, + input wire clk, + + // BAR write interface + input wire [31:0] wr_addr, + input wire [3:0] wr_be, + input wire [31:0] wr_data, + input wire wr_valid, + + // BAR read interface + input wire [87:0] rd_req_ctx, + input wire [31:0] rd_req_addr, + input wire rd_req_valid, + output reg [87:0] rd_rsp_ctx, + output reg [31:0] rd_rsp_data, + output reg rd_rsp_valid +); + + // ========================================================================= + // Register Address Definitions + // ========================================================================= +{% for reg_name, reg_def in behavioral_spec.registers.items() %} + localparam REG_{{ reg_name | upper }} = 32'h{{ "%04X" | format(reg_def.offset) }}; +{% endfor %} + + // ========================================================================= + // Register and Counter Declarations + // ========================================================================= +{% for reg_name, reg_def in behavioral_spec.registers.items() %} + reg [31:0] {{ reg_name }}_reg; +{% endfor %} + +{% for counter_name, counter_def in behavioral_spec.counters.items() %} + reg [{{ counter_def.width - 1 }}:0] {{ counter_name }}; +{% endfor %} + + // ========================================================================= + // Behavioral Simulation Logic + // ========================================================================= + always_ff @(posedge clk or posedge rst) begin + if (rst) begin + // Reset all registers to default values +{% for reg_name, reg_def in behavioral_spec.registers.items() %} + {% if reg_def.behavior == "constant" %} + {{ reg_name }}_reg <= 32'h{{ "%08X" | format(reg_def.default) }}; + {% elif reg_def.behavior == "auto_increment" %} + {{ reg_name }}_reg <= 32'h{{ "%08X" | format(reg_def.default) }}; + {% elif reg_def.behavior == "write_capture" %} + {{ reg_name }}_reg <= 32'h{{ "%08X" | format(reg_def.default) }}; + {% else %} + {{ reg_name }}_reg <= 32'h{{ "%08X" | format(reg_def.default) }}; + {% endif %} +{% endfor %} + + // Reset all counters +{% for counter_name, counter_def in behavioral_spec.counters.items() %} + {{ counter_name }} <= {{ counter_def.width }}'h{{ "%X" | format(counter_def.reset_value) }}; +{% endfor %} + end else begin + // Update counters +{% for counter_name, counter_def in behavioral_spec.counters.items() %} + {{ counter_name }} <= {{ counter_name }} + {{ counter_def.increment_rate }}; +{% endfor %} + + // Update auto-increment registers +{% for reg_name, reg_def in behavioral_spec.registers.items() %} + {% if reg_def.behavior == "auto_increment" %} + {{ reg_name }}_reg <= {{ reg_def.pattern }}; + {% endif %} +{% endfor %} + + // Handle write captures + if (wr_valid) begin + case (wr_addr) +{% for reg_name, reg_def in behavioral_spec.registers.items() %} + {% if reg_def.behavior == "write_capture" and not reg_def.read_only %} + REG_{{ reg_name | upper }}: begin + {{ reg_name }}_reg <= wr_data; + end + {% endif %} +{% endfor %} + default: begin + // Ignore writes to undefined registers + end + endcase + end + end + end + + // ========================================================================= + // Read Response Logic + // ========================================================================= + always_ff @(posedge clk or posedge rst) begin + if (rst) begin + rd_rsp_ctx <= 88'h0; + rd_rsp_data <= 32'h0; + rd_rsp_valid <= 1'b0; + end else if (rd_req_valid) begin + rd_rsp_ctx <= rd_req_ctx; + rd_rsp_valid <= 1'b1; + + case (rd_req_addr) +{% for reg_name, reg_def in behavioral_spec.registers.items() %} + REG_{{ reg_name | upper }}: rd_rsp_data <= {{ reg_name }}_reg; +{% endfor %} + default: rd_rsp_data <= 32'hDEADBEEF; // Undefined register + endcase + end else begin + rd_rsp_valid <= 1'b0; + end + end + +endmodule + +`default_nettype wire diff --git a/src/templates/sv/cfg_shadow.sv.j2 b/src/templates/sv/cfg_shadow.sv.j2 index 5b319345..c707f2d3 100644 --- a/src/templates/sv/cfg_shadow.sv.j2 +++ b/src/templates/sv/cfg_shadow.sv.j2 @@ -76,7 +76,7 @@ module pcileech_tlps128_cfgspace_shadow #( // Main configuration space BRAM ({{ CONFIG_SPACE_SIZE }} bytes = {{ CONFIG_SPACE_SIZE // 4 }} dwords) (* ram_style="block" *) - (* ram_init_file = CFG_INIT_HEX *) // BRAM initialization attribute (tool-resolved string constant) + // BRAM initialization via $readmemh in initial block (string parameters not supported in attributes) logic [31:0] config_space_ram[0:{{ (CONFIG_SPACE_SIZE // 4) - 1 }}]; // Enhanced overlay RAM with bit type support ({{ OVERLAY_ENTRIES }} entries) @@ -99,17 +99,11 @@ module pcileech_tlps128_cfgspace_shadow #( if (ENABLE_SPARSE_MAP) begin : gen_sparse_map end - // Hash table entry for sparse mapping - typedef struct packed { - logic valid; // Entry is valid - logic [9:0] reg_num; // Original register number - logic [7:0] overlay_index; // Index into overlay RAM - } hash_entry_t; - - // Hash table for sparse mapping - declared unconditionally for tool compatibility - // When ENABLE_SPARSE_MAP=0, synthesis should prune unused logic; functional - // accesses are already gated under ENABLE_SPARSE_MAP conditionals. - (* ram_style="distributed" *) hash_entry_t hash_table[0:HASH_TABLE_SIZE-1]; + // Hash table entry for sparse mapping - using separate arrays for synthesis compatibility + // SystemVerilog unpacked arrays of structs cannot have ram_style attributes applied directly + (* ram_style="distributed" *) logic hash_table_valid[0:HASH_TABLE_SIZE-1]; + (* ram_style="distributed" *) logic [9:0] hash_table_reg_num[0:HASH_TABLE_SIZE-1]; + (* ram_style="distributed" *) logic [7:0] hash_table_overlay_index[0:HASH_TABLE_SIZE-1]; // Hash function for sparse mapping function automatic logic [7:0] hash_reg_num(input logic [9:0] reg_num); @@ -125,11 +119,11 @@ module pcileech_tlps128_cfgspace_shadow #( // Linear probe with wrap-around while (probe_count < HASH_TABLE_SIZE) begin - if (hash_table[hash_idx].valid && - hash_table[hash_idx].reg_num == reg_num) begin - return hash_table[hash_idx].overlay_index; + if (hash_table_valid[hash_idx] && + hash_table_reg_num[hash_idx] == reg_num) begin + return hash_table_overlay_index[hash_idx]; end - if (!hash_table[hash_idx].valid) begin + if (!hash_table_valid[hash_idx]) begin return 8'hFF; // Not found end hash_idx = (hash_idx + 1) & (HASH_TABLE_SIZE-1); @@ -338,7 +332,7 @@ module pcileech_tlps128_cfgspace_shadow #( // Initialize hash table if sparse mapping enabled if (ENABLE_SPARSE_MAP) begin for (int i = 0; i < HASH_TABLE_SIZE; i++) begin - hash_table[i].valid <= 1'b0; + hash_table_valid[i] <= 1'b0; end end end else begin @@ -662,7 +656,9 @@ module pcileech_tlps128_cfgspace_shadow #( // Initialize hash table for sparse mapping if (ENABLE_SPARSE_MAP) begin for (int i = 0; i < HASH_TABLE_SIZE; i++) begin - hash_table[i] = '{valid: 1'b0, reg_num: 10'h0, overlay_index: 8'h0}; + hash_table_valid[i] = 1'b0; + hash_table_reg_num[i] = 10'h0; + hash_table_overlay_index[i] = 8'h0; end // Populate hash table with configured overlays @@ -674,16 +670,14 @@ module pcileech_tlps128_cfgspace_shadow #( logic [7:0] hash_idx = hash_reg_num({{ reg }}); logic [7:0] probe_count = 0; // Find empty slot using linear probing - while (hash_table[hash_idx].valid && probe_count < HASH_TABLE_SIZE) begin + while (hash_table_valid[hash_idx] && probe_count < HASH_TABLE_SIZE) begin hash_idx = (hash_idx + 1) & (HASH_TABLE_SIZE-1); probe_count++; end - if (!hash_table[hash_idx].valid) begin - hash_table[hash_idx] = '{ - valid: 1'b1, - reg_num: {{ reg }}, - overlay_index: {{ loop.index0 }} - }; + if (!hash_table_valid[hash_idx]) begin + hash_table_valid[hash_idx] = 1'b1; + hash_table_reg_num[hash_idx] = {{ reg }}; + hash_table_overlay_index[hash_idx] = {{ loop.index0 }}; end end {%- endfor -%} @@ -696,16 +690,14 @@ module pcileech_tlps128_cfgspace_shadow #( logic [7:0] hash_idx = hash_reg_num({{ reg }}); logic [7:0] probe_count = 0; // Find empty slot using linear probing - while (hash_table[hash_idx].valid && probe_count < HASH_TABLE_SIZE) begin + while (hash_table_valid[hash_idx] && probe_count < HASH_TABLE_SIZE) begin hash_idx = (hash_idx + 1) & (HASH_TABLE_SIZE-1); probe_count++; end - if (!hash_table[hash_idx].valid) begin - hash_table[hash_idx] = '{ - valid: 1'b1, - reg_num: {{ reg }}, - overlay_index: {{ loop.index0 }} - }; + if (!hash_table_valid[hash_idx]) begin + hash_table_valid[hash_idx] = 1'b1; + hash_table_reg_num[hash_idx] = {{ reg }}; + hash_table_overlay_index[hash_idx] = {{ loop.index0 }}; end end {%- endif -%} diff --git a/src/templates/sv/pcie_endpoint_defines.sv.j2 b/src/templates/sv/pcie_endpoint_defines.sv.j2 new file mode 100644 index 00000000..ef8d6411 --- /dev/null +++ b/src/templates/sv/pcie_endpoint_defines.sv.j2 @@ -0,0 +1,47 @@ +{% from "_helpers.j2" import safe_attr %} +{# PCIe Endpoint Device-Specific Defines + + This template generates device-specific defines for PCIe endpoint configuration, + particularly for Device Serial Number (DSN) and OUI (Organizationally Unique Identifier). + + The DSN is a 64-bit value composed of two 32-bit parts: + - Upper 32 bits (DSN_2): Device-specific serial number + - Lower 32 bits (DSN_1): Typically contains OUI in bits [23:0] + + Reference: PCIe Base Specification, Device Serial Number Extended Capability +#} + +// +// IEEE Organizationally Unique Identifier (OUI) +// Extracted from vendor ID: {{ vendor_id | default('0x0000') }} +// +{%- set _oui = vendor_oui | default(0) %} +`define PCI_EXP_EP_OUI 24'h{{ "%06X" | format(_oui) }} + +// +// Device Serial Number (DSN) constants +// DSN: {{ device_serial_number_hex | default('0x0000000000000000') }} +// +{%- set _dsn_upper = pci_exp_ep_dsn_2 | default(dsn_upper_32) | default(device_serial_number_hi) | default(0) %} +{%- set _dsn_lower = pci_exp_ep_dsn_1 | default(dsn_lower_32) | default(device_serial_number_lo) | default(0) %} +`define PCI_EXP_EP_DSN_2 32'h{{ "%08X" | format(_dsn_upper) }} +{%- if device_serial_number_valid | default(false) %} +`define PCI_EXP_EP_DSN_1 32'h{{ "%08X" | format(_dsn_lower) }} +{%- else %} +// NOTE: Donor did not provide DSN; constructing from OUI +`define PCI_EXP_EP_DSN_1 {{ "{" }} 8'h01, `PCI_EXP_EP_OUI {{ "}" }} +{%- endif %} + +// +// Semantic DSN components for advanced use cases +// +{%- set _dsn_oui = dsn_oui | default(0) %} +{%- set _dsn_ext = dsn_extension | default(1) %} +`define PCI_EXP_EP_DSN_OUI 24'h{{ "%06X" | format(_dsn_oui) }} +`define PCI_EXP_EP_DSN_EXT 8'h{{ "%02X" | format(_dsn_ext) }} + +// +// Complete DSN assignment macro +// Usage: assign cfg_dsn = `PCI_EXP_EP_DSN_FULL; +// +`define PCI_EXP_EP_DSN_FULL {{ "{" }} `PCI_EXP_EP_DSN_2, `PCI_EXP_EP_DSN_1 {{ "}" }} diff --git a/src/templates/sv/top_level/bar_controller_block.sv.j2 b/src/templates/sv/top_level/bar_controller_block.sv.j2 index a49b0161..ee9975bb 100644 --- a/src/templates/sv/top_level/bar_controller_block.sv.j2 +++ b/src/templates/sv/top_level/bar_controller_block.sv.j2 @@ -47,8 +47,7 @@ assign cfg_mgmt_wr_en = 1'b0; assign cfg_mgmt_rd_en = 1'b0; assign cfg_mgmt_wr_readonly = 1'b0; - assign cfg_mgmt_wr_rw1c_as_rw = 1'b1; - assign cfg_dsn = DEVICE_SERIAL_NUMBER; + // cfg_mgmt_wr_rw1c_as_rw and cfg_dsn are tied off in internal_signals.sv.j2 // AXI-Stream control signals - removed static tie-offs for proper handling // s_axis_tx_tkeep and s_axis_tx_tlast are now driven by TLP completion logic diff --git a/src/templates/sv/top_level/internal_signals.sv.j2 b/src/templates/sv/top_level/internal_signals.sv.j2 index 575f11c7..835f56f6 100644 --- a/src/templates/sv/top_level/internal_signals.sv.j2 +++ b/src/templates/sv/top_level/internal_signals.sv.j2 @@ -42,13 +42,43 @@ logic cfg_mgmt_wr_en; logic cfg_mgmt_rd_en; logic cfg_mgmt_wr_readonly; - logic cfg_mgmt_wr_rw1c_as_rw; + + // Config space access handshaking (for systems with cfg read/write) + // These signals facilitate config space read operations from external logic + logic m_axis_cfg_tx_tready; + logic [31:0] m_axis_cfg_tx_tdata; + logic s_axis_cfg_rx_tlast; + logic [3:0] s_axis_cfg_rx_tkeep; + + // Default handshaking values (ready to accept config transactions) + assign m_axis_cfg_tx_tready = 1'b1; + assign s_axis_cfg_rx_tlast = 1'b1; + assign s_axis_cfg_rx_tkeep = 4'b1111; + + // Extract DWORD address from config transaction data when available + // This allows external logic to parse config space access addresses + assign cfg_mgmt_dwaddr = cfg_mgmt_rd_en ? m_axis_cfg_tx_tdata[9:0] : 10'h0; + logic [63:0] cfg_dsn; localparam logic [63:0] DEVICE_SERIAL_NUMBER = 64'h{{ _local_dsn_hex }}; {%- if not _local_dsn_valid %} // NOTE: Donor did not report a DSN; cfg_dsn defaults to zero for compliance. {%- endif %} + // Tie-off cfg_mgmt_wr_rw1c_as_rw immediately (must be driven before PCIe core instantiation) + wire cfg_mgmt_wr_rw1c_as_rw = 1'b1; + assign cfg_dsn = DEVICE_SERIAL_NUMBER; + + // Bus/Device/Function tracking (outputs from PCIe core) + logic [7:0] cfg_bus_number; + logic [4:0] cfg_device_number; + logic [2:0] cfg_function_number; + + // Completer ID (BDF) - 16-bit identifier composed of Bus/Device/Function + // Format: { bus[7:0], device[4:0], function[2:0] } + wire [15:0] completer_id; + assign completer_id = {cfg_bus_number, cfg_device_number, cfg_function_number}; + // Interrupt status (outputs) and INTx/MSI inputs (tied off if MSI-X only) logic cfg_interrupt_msixenable; logic cfg_interrupt_msixfm; diff --git a/src/templates/tcl/constraints.j2 b/src/templates/tcl/constraints.j2 index 8ff2ecd5..e72b0cf6 100644 --- a/src/templates/tcl/constraints.j2 +++ b/src/templates/tcl/constraints.j2 @@ -77,18 +77,21 @@ puts "Using generic timing constraints only. You may need to add board-specific {% endif %} {% set sys_clk_period = (1000.0 / _sys_clk_mhz) | round(3) %} set timing_constraints_content { +# Device-specific timing constraints for {{ device.vendor_id }}:{{ device.device_id }} +# Board: {{ board_name(board) }} +# System Clock: {{ _sys_clk_mhz }} MHz (period: {{ sys_clk_period }} ns) + # PCIe Transceiver Pin Constraints -# NOTE: Actual pin assignments depend on target board and must be set in board-specific XDC -# The following are placeholders - board XDC should override these +# NOTE: Pin assignments MUST be loaded from board-specific XDC files +# The following are commented placeholders for reference only -# PCIe transceiver differential pairs (adjust for your target board) -# Example for Artix-7 100T: +# PCIe transceiver differential pairs (board-specific) # set_property PACKAGE_PIN B8 [get_ports {pci_exp_rxp[0]}] # set_property PACKAGE_PIN A8 [get_ports {pci_exp_rxn[0]}] # set_property PACKAGE_PIN B6 [get_ports {pci_exp_txp[0]}] # set_property PACKAGE_PIN A6 [get_ports {pci_exp_txn[0]}] -# System clock differential input ({{ _sys_clk_mhz }} MHz) +# System clock differential input # set_property PACKAGE_PIN E19 [get_ports sys_clk_p] # set_property PACKAGE_PIN E18 [get_ports sys_clk_n] # set_property IOSTANDARD DIFF_SSTL15 [get_ports sys_clk_p] @@ -98,24 +101,14 @@ set timing_constraints_content { # set_property PACKAGE_PIN U7 [get_ports sys_rst_n] # set_property IOSTANDARD LVCMOS33 [get_ports sys_rst_n] -# Clock constraints - {{ _sys_clk_mhz }} MHz system clock +# Timing Constraints create_clock -period {{ sys_clk_period }} -name sys_clk_p [get_ports sys_clk_p] - -# PCIe reference clock constraint (100 MHz typical) -# This will be generated by the PCIe IP core -# create_clock -period 10.000 -name pcie_refclk [get_pins pcie_core/pcie_clk] - -# Input delay constraints for actual input ports set_input_delay -clock sys_clk_p 2.000 [get_ports sys_rst_n] - -# False path for asynchronous reset set_false_path -from [get_ports sys_rst_n] -# Device-specific constraints for {{ device.vendor_id }}:{{ device.device_id }} -# Board-specific pin assignments for {{ board_name(board) }} - -# NOTE: Pin assignments MUST be loaded from board-specific XDC files from the PCILeech repository. -# We NEVER generate fallback XDC content as it contains critical pin assignments. +# PCIe IP Core Clocks (generated by IP, constraints added automatically) +# Reference: refclk={{ pcie_refclk_freq | default('100 MHz') }} +# User clocks: userclk1={{ pcie_userclk1_freq | default('N/A') }}, userclk2={{ pcie_userclk2_freq | default('N/A') }} } # Write timing constraints to file diff --git a/src/templates/tcl/ip_config.j2 b/src/templates/tcl/ip_config.j2 index b27ac507..59e39083 100644 --- a/src/templates/tcl/ip_config.j2 +++ b/src/templates/tcl/ip_config.j2 @@ -109,6 +109,12 @@ safe_set $ip CONFIG.Legacy_Interrupt {NONE} # MSI/MSIX configuration: disable MSI, enable MSIX safe_set $ip CONFIG.MSI_Enabled {false} safe_set $ip CONFIG.MSIX_Enabled {true} +# PCIe clock configuration for 7-series +# PCIE_REFCLK_FREQ: 0=100MHz, 1=125MHz, 2=250MHz +safe_set $ip CONFIG.PCIE_EXT_CLK_FREQ {{ pcie_refclk_freq }} +# PCIE_USERCLK1_FREQ and PCIE_USERCLK2_FREQ: 1=31.25MHz, 2=62.5MHz, 3=125MHz, 4=250MHz, 5=500MHz +safe_set $ip CONFIG.User_Clk_Freq {{ pcie_userclk1_freq }} +safe_set $ip CONFIG.User_Clk2_Freq {{ pcie_userclk2_freq }} # Generate IP targets for complete synthesis flow generate_target {instantiation_template synthesis implementation} $ip diff --git a/src/templates/tcl/pcileech_generate_project.j2 b/src/templates/tcl/pcileech_generate_project.j2 index b3e93e46..65724780 100644 --- a/src/templates/tcl/pcileech_generate_project.j2 +++ b/src/templates/tcl/pcileech_generate_project.j2 @@ -1,6 +1,16 @@ {% from "_helpers.j2" import get_vendor_id, get_device_id, get_subsystem_vendor_id, get_subsystem_device_id %} {{ header_comment }} + +# Set the reference directory for source file relative paths +set origin_dir "." + +# Use origin directory path location variable, if specified in the tcl shell +if { [info exists ::origin_dir_loc] } { + set origin_dir $::origin_dir_loc +} + puts "Generating project" +puts "Origin directory: $origin_dir" set_msg_config -id {Common 17-55} -suppress set_msg_config -id {Vivado 12-1387} -suppress # End of error handling configuration @@ -24,14 +34,32 @@ catch {close_project -quiet} # Create Vivado project create_project $project_name $project_dir -part $fpga_part -force -# Set project properties (Vivado supports Verilog/VHDL at project level) -set_property target_language Verilog [current_project] -set_property simulator_language Mixed [current_project] -set_property default_lib xil_defaultlib [current_project] +# Set project properties - comprehensive configuration for robust builds +set obj [current_project] + +# Core language and library settings +set_property -name "target_language" -value "Verilog" -objects $obj +set_property -name "simulator_language" -value "Mixed" -objects $obj +set_property -name "default_lib" -value "xil_defaultlib" -objects $obj -{% if fpga_family == "ultrascale" or fpga_family == "ultrascale_plus" %} -# UltraScale/UltraScale+ specific settings -set_property XPM_LIBRARIES {XPM_CDC XPM_MEMORY XPM_FIFO} [current_project] +# XPM Libraries - CRITICAL: Required for all FPGA families (FIFO, CDC, Memory primitives) +set_property -name "xpm_libraries" -value "XPM_CDC XPM_FIFO XPM_MEMORY" -objects $obj + +# IP cache and output management - prevents cache corruption and build errors +set_property -name "ip_cache_permissions" -value "read write" -objects $obj +set_property -name "ip_output_repo" -value "$project_dir/${project_name}.cache/ip" -objects $obj + +# Simulation configuration - enables proper simulation file management +set_property -name "sim.central_dir" -value "$project_dir/${project_name}.ip_user_files" -objects $obj +set_property -name "sim.ip.auto_export_scripts" -value "1" -objects $obj + +# Memory map generation - useful for debugging and verification +set_property -name "mem.enable_memory_map_generation" -value "1" -objects $obj + +{% if board_part_id %} +# Board-specific configuration (if board part specified) +set_property -name "board_part" -value "{{ board_part_id }}" -objects $obj +set_property -name "platform.board_id" -value "{{ board_name }}" -objects $obj {% endif %} # Create source directory structure @@ -42,6 +70,22 @@ puts "Created PCILeech directory structure:" puts " Source directory: $src_dir" puts " IP directory: $ip_dir" +# Create filesets with existence checks (prevents conflicts) +if {[string equal [get_filesets -quiet sources_1] ""]} { + create_fileset -srcset sources_1 + puts "Created sources_1 fileset" +} + +if {[string equal [get_filesets -quiet constrs_1] ""]} { + create_fileset -constrset constrs_1 + puts "Created constrs_1 fileset" +} + +if {[string equal [get_filesets -quiet sim_1] ""]} { + create_fileset -simset sim_1 + puts "Created sim_1 fileset" +} + # Add SystemVerilog source files {% if pcileech.source_files %} puts "Adding SystemVerilog source files..." @@ -290,6 +334,27 @@ puts "Generating IP cores..." generate_target all [get_files *.xci] catch {config_ip_cache -export [get_ips -all]} +# Configure IP file properties for proper synthesis and checkpoint management +puts "Configuring IP file properties..." +set ip_files [get_files -filter {FILE_TYPE == "IP"}] +if {[llength $ip_files] > 0} { + foreach ip_file $ip_files { + # Prevent reference file generation (keeps project cleaner) + set_property -name "generate_files_for_reference" -value "0" -objects $ip_file + + # Register with IP catalog manager for proper tracking + set_property -name "registered_with_manager" -value "1" -objects $ip_file + + # Set synthesis checkpoint mode (only if IP is not locked) + if { ![get_property "is_locked" $ip_file] } { + set_property -name "synth_checkpoint_mode" -value "Singular" -objects $ip_file + } + } + puts " Configured [llength $ip_files] IP file(s)" +} else { + puts " No IP files found to configure" +} + # Skip validation (validate_design is not available in all Vivado versions) puts "Skipping design validation..." # validate_design -quiet diff --git a/src/templating/sv_context_builder.py b/src/templating/sv_context_builder.py index 2879e134..26c17d67 100644 --- a/src/templating/sv_context_builder.py +++ b/src/templating/sv_context_builder.py @@ -241,6 +241,27 @@ def _add_device_identification( device_config["vendor_id_int"] = context["vendor_id_int"] device_config["device_id_int"] = context["device_id_int"] + # Extract IEEE OUI from vendor ID + self._add_vendor_oui(context, context["vendor_id_int"]) + + def _add_vendor_oui(self, context: Dict[str, Any], vendor_id_int: int) -> None: + """ + Extract IEEE OUI (Organizationally Unique Identifier) from vendor ID. + + The OUI is typically the lower 24 bits of the vendor ID. + This is used in DSN construction per PCIe specification. + + Args: + context: Context dictionary to update + vendor_id_int: Vendor ID as integer + """ + # OUI is 24-bit identifier from vendor ID + oui = vendor_id_int & 0xFFFFFF + + context["vendor_oui"] = oui + context["vendor_oui_hex"] = safe_format("0x{value:06X}", value=oui) + context["pci_exp_ep_oui"] = oui # Match reference implementation naming + def _add_device_serial_number( self, context: Dict[str, Any], template_context: Dict[str, Any] ) -> None: @@ -269,6 +290,52 @@ def _add_device_serial_number( context["device_serial_number_hi"] = (serial_int >> 32) & 0xFFFFFFFF context["device_serial_number_lo"] = serial_int & 0xFFFFFFFF + # Semantic decomposition of DSN into OUI + serial components + # Per PCIe spec: DSN lower 32 bits often contain OUI in bits [23:0] + self._add_dsn_semantic_fields(context, serial_int) + + def _add_dsn_semantic_fields( + self, context: Dict[str, Any], dsn_value: int + ) -> None: + """ + Decompose DSN into semantic components for SystemVerilog defines. + + Per PCIe specification, DSN is typically composed as: + - Upper 32 bits: Device-specific serial number + - Lower 32 bits: Often contains OUI in bits [23:0] + additional data + + This matches the reference implementation pattern: + `define PCI_EXP_EP_DSN_2 32'h00000001 + `define PCI_EXP_EP_DSN_1 {{ 8'h1 }, `PCI_EXP_EP_OUI } + + Args: + context: Context dictionary to update + dsn_value: 64-bit DSN value + """ + # Extract semantic components + dsn_upper = (dsn_value >> 32) & 0xFFFFFFFF # Upper 32 bits + dsn_lower = dsn_value & 0xFFFFFFFF # Lower 32 bits + + # Extract OUI from lower 32 bits (typically bits [23:0]) + dsn_oui = dsn_lower & 0xFFFFFF + + # Extract extension field from lower 32 bits (typically bits [31:24]) + dsn_ext = (dsn_lower >> 24) & 0xFF + + # Add fields for SystemVerilog template use + context["dsn_upper_32"] = dsn_upper + context["dsn_lower_32"] = dsn_lower + context["dsn_oui"] = dsn_oui + context["dsn_oui_hex"] = safe_format("0x{value:06X}", value=dsn_oui) + context["dsn_extension"] = dsn_ext + context["dsn_extension_hex"] = safe_format("0x{value:02X}", value=dsn_ext) + + # Add PCI Express endpoint define names (match reference implementation) + context["pci_exp_ep_dsn_2"] = dsn_upper + context["pci_exp_ep_dsn_1"] = dsn_lower + context["pci_exp_ep_dsn_2_hex"] = safe_format("32'h{value:08X}", value=dsn_upper) + context["pci_exp_ep_dsn_1_hex"] = safe_format("32'h{value:08X}", value=dsn_lower) + def _extract_device_serial_number(self, template_context: Dict[str, Any]) -> Any: """Gather DSN candidates from the template context.""" diff --git a/src/templating/tcl_builder.py b/src/templating/tcl_builder.py index f275f84f..fbee5b92 100644 --- a/src/templating/tcl_builder.py +++ b/src/templating/tcl_builder.py @@ -421,6 +421,18 @@ def _validate_enums(ip_type: str, speed_enum: str, width_enum: str) -> None: # Validate derived enums against the IP type; fail fast on mismatch _validate_enums(self.pcie_ip_type, derived_speed, derived_width) + # Map board name to official Xilinx board part ID (if available) + # This enables board-specific optimizations and constraints + board_part_id_map = { + # Note: Most PCILeech boards don't have official Xilinx board parts + # They use raw FPGA parts instead. Add mappings here if using + # official dev boards (AC701, KC705, ZC706, etc.) + # "ac701": "xilinx.com:ac701:part0:1.4", + # "kc705": "xilinx.com:kc705:part0:1.6", + # "zc706": "xilinx.com:zc706:part0:1.4", + } + board_part_id = board_part_id_map.get(self.board_name.lower()) + return { # REQUIRED VARIABLES - These are critical for template validation "device_signature": device_signature, @@ -511,6 +523,8 @@ def _validate_enums(ip_type: str, speed_enum: str, width_enum: str) -> None: "pcileech_ip_dir": self.pcileech_ip_dir, "batch_mode": self.batch_mode, "constraint_files": [], # Add empty constraint files list + # Board part ID for Xilinx board-specific optimizations (optional) + "board_part_id": board_part_id, # Link configuration propagated to templates "target_link_speed": derived_speed, "target_link_width_enum": derived_width, diff --git a/src/utils/behavioral_context.py b/src/utils/behavioral_context.py new file mode 100644 index 00000000..655d447a --- /dev/null +++ b/src/utils/behavioral_context.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""Behavioral simulation context integration.""" + +import logging +from typing import Dict, Any, Optional + +from src.string_utils import log_info_safe, log_warning_safe, log_debug_safe, safe_format +from src.behavioral.analyzer import BehavioralAnalyzerFactory +from src.behavioral.base import require + + +logger = logging.getLogger(__name__) + + +def build_behavioral_context(device_config: Any) -> Dict[str, Any]: + """Build behavioral simulation context for device.""" + if not getattr(device_config, 'enable_behavioral_simulation', False): + log_debug_safe(logger, "Behavioral simulation disabled") + # Return minimal context for template compatibility + return { + "enable_behavioral_simulation": False, + "behavioral_spec": { + "device_category": "generic", + "registers": {}, + "counters": {} + } + } + + log_info_safe(logger, "Building behavioral simulation context") + + # Generate behavioral specification + spec = BehavioralAnalyzerFactory.generate_behavioral_spec(device_config) + if not spec: + log_warning_safe(logger, "No behavioral spec generated for device") + return {} + + # Validate specification + require(spec.validate(), "Invalid behavioral specification") + + # Convert to template-compatible format + context = { + "enable_behavioral_simulation": True, + "behavioral_spec": spec.to_dict(), + "behavioral_bar_index": getattr(device_config, 'behavioral_bar_index', 0) + } + + log_info_safe(logger, safe_format( + "Generated behavioral context with {reg_count} registers, {cnt_count} counters", + reg_count=len(spec.registers), + cnt_count=len(spec.counters) + )) + + return context + + +def integrate_behavioral_context(base_context: Dict[str, Any], + device_config: Any) -> Dict[str, Any]: + """Integrate behavioral context into base context.""" + behavioral_ctx = build_behavioral_context(device_config) + + if behavioral_ctx: + base_context.update(behavioral_ctx) + log_info_safe(logger, "Integrated behavioral simulation context") + + return base_context diff --git a/src/utils/log_phases.py b/src/utils/log_phases.py deleted file mode 100644 index 245f5746..00000000 --- a/src/utils/log_phases.py +++ /dev/null @@ -1,82 +0,0 @@ -"""Phase logging utilities to standardize build/status output. - -Minimal, dependency-light helper consolidating repeated glyph + formatting -patterns used across build flows. Avoids hardcoding icons in multiple files. -""" - -from __future__ import annotations - -import time -from typing import Optional - -from src.string_utils import log_info_safe, safe_format - -GLYPHS = { - "start": "➤", - "ok": "✓", - "warn": "⚠", - "step": "•", -} - - -class PhaseLogger: - """Lightweight phase logger accumulating timing for phases.""" - - def __init__(self, logger, emit_durations: bool = True): - self.logger = logger - self.emit_durations = emit_durations - self._active_name: Optional[str] = None - self._start_ts: float = 0.0 - - def begin(self, name: str, message: Optional[str] = None): - self._finish_if_active() - self._active_name = name - self._start_ts = time.perf_counter() - log_info_safe( - self.logger, - safe_format("{g} {msg}", g=GLYPHS["start"], msg=message or name), - prefix="BUILD", - ) - - def step(self, message: str): - log_info_safe( - self.logger, - safe_format(" {g} {msg}", g=GLYPHS["step"], msg=message), - prefix="BUILD", - ) - - def success(self, message: Optional[str] = None): - if not self._active_name: - return - dur = time.perf_counter() - self._start_ts - if self.emit_durations: - log_info_safe( - self.logger, - safe_format( - "{g} {name} {ok} ({sec:.1f}s)", - g=GLYPHS["ok"], - name=self._active_name, - ok=message or "done", - sec=dur, - ), - prefix="BUILD", - ) - else: - log_info_safe( - self.logger, - safe_format( - "{g} {name} {ok}", - g=GLYPHS["ok"], - name=self._active_name, - ok=message or "done", - ), - prefix="BUILD", - ) - self._active_name = None - - def _finish_if_active(self): # internal safety - if self._active_name: - self.success("(auto)") - - -__all__ = ["PhaseLogger", "GLYPHS"] diff --git a/src/utils/unified_context.py b/src/utils/unified_context.py index 52d645b8..ba02fcd0 100644 --- a/src/utils/unified_context.py +++ b/src/utils/unified_context.py @@ -69,6 +69,15 @@ "enable_clock_gating": False, } +# PCIe clock configuration for Xilinx 7-series cores +DEFAULT_PCIE_CLOCK_CONFIG = { + "pcie_refclk_freq": 0, # 0=100MHz, 1=125MHz, 2=250MHz + "pcie_userclk1_freq": 2, # 1=31.25MHz, 2=62.5MHz, 3=125MHz, 4=250MHz, 5=500MHz + "pcie_userclk2_freq": 2, # Same encoding as userclk1 + "pcie_link_speed": 2, # 1=Gen1, 2=Gen2, 3=Gen3 + "pcie_oobclk_mode": 1, # OOB clock mode +} + # Defaults for PCILeech-specific runtime configuration used by templates PCILEECH_DEFAULT = { "buffer_size": 4096, @@ -698,6 +707,12 @@ def create_board_config( **kwargs, ) -> TemplateObject: """Create board configuration for templates.""" + # Extract PCIe clock config from kwargs or use defaults + pcie_clock_config = dict(DEFAULT_PCIE_CLOCK_CONFIG) + for key in DEFAULT_PCIE_CLOCK_CONFIG.keys(): + if key in kwargs: + pcie_clock_config[key] = kwargs[key] + config = { "name": board_name, "fpga_part": fpga_part, @@ -712,6 +727,8 @@ def create_board_config( ), "features": kwargs.get("features", {}), } + # Add PCIe clock configuration + config.update(pcie_clock_config) config.update(kwargs) return TemplateObject(config) @@ -1157,6 +1174,21 @@ def _add_standard_configs(self, context: Dict[str, Any], **kwargs) -> None: ) context["timing_config"] = TemplateObject(timing_config) + # PCIe clock configuration for Xilinx 7-series + pcie_clock_config = dict(DEFAULT_PCIE_CLOCK_CONFIG) + pcie_clock_config.update( + { + "pcie_refclk_freq": kwargs.get("pcie_refclk_freq", 0), # 0=100MHz + "pcie_userclk1_freq": kwargs.get("pcie_userclk1_freq", 2), # 2=62.5MHz + "pcie_userclk2_freq": kwargs.get("pcie_userclk2_freq", 2), # 2=62.5MHz + "pcie_link_speed": kwargs.get("pcie_link_speed", 2), # 2=Gen2 + "pcie_oobclk_mode": kwargs.get("pcie_oobclk_mode", 1), + } + ) + context["pcie_clock_config"] = TemplateObject(pcie_clock_config) + # Also add individual keys to root context for template access + context.update(pcie_clock_config) + # PCILeech configuration # Check for explicit scatter_gather setting, with fallback to DMA operations scatter_gather_enabled = kwargs.get( @@ -1213,6 +1245,11 @@ def _add_standard_configs(self, context: Dict[str, Any], **kwargs) -> None: "target_link_width_enum", "X4" ) # x4 lanes default + # Board part ID for Xilinx dev boards (None for custom PCILeech boards) + # This enables board-specific optimizations when using official Xilinx boards + # Most PCILeech boards use raw FPGA parts, so None is the typical value + context["board_part_id"] = kwargs.get("board_part_id", None) + def _add_compatibility_aliases(self, context: Dict[str, Any], **kwargs) -> None: """Add compatibility aliases for legacy templates.""" # Update the main config object with error handling and performance @@ -1468,6 +1505,44 @@ def _coerce_toggle(value: Any, default: int) -> int: ) context.setdefault("is_64bit", kwargs.get("is_64bit", False)) + def _add_behavioral_context(self, context: Dict[str, Any], **kwargs) -> None: + """Add behavioral simulation context if enabled.""" + try: + from src.utils.behavioral_context import build_behavioral_context + + # Create mock device config from context + class DeviceConfig: + def __init__(self, ctx, kw): + self.enable_behavioral_simulation = kw.get( + "enable_behavioral_simulation", False + ) + self.class_code = int(kw.get("class_code", "000000"), 16) + self.device_id = kw.get("device_id", "0000") + self.behavioral_bar_index = kw.get("behavioral_bar_index", 0) + + device_config = DeviceConfig(context, kwargs) + behavioral_ctx = build_behavioral_context(device_config) + + if behavioral_ctx: + context.update(behavioral_ctx) + log_info_safe( + self.logger, + "Behavioral simulation context integrated", + prefix="BUILD" + ) + except ImportError as e: + log_debug_safe( + self.logger, + safe_format("Behavioral module not available: {e}", e=e), + prefix="BUILD" + ) + except Exception as e: + log_warning_safe( + self.logger, + safe_format("Failed to add behavioral context: {e}", e=e), + prefix="BUILD" + ) + def create_complete_template_context( self, vendor_id: Optional[str] = None, @@ -1533,6 +1608,9 @@ def create_complete_template_context( # Add standard configurations self._add_standard_configs(context, **kwargs) + # Add behavioral simulation context if enabled + self._add_behavioral_context(context, **kwargs) + # Apply any additional kwargs context.update(kwargs) diff --git a/tests/test_behavioral.py b/tests/test_behavioral.py new file mode 100644 index 00000000..acc7c7c9 --- /dev/null +++ b/tests/test_behavioral.py @@ -0,0 +1,638 @@ +#!/usr/bin/env python3 +"""Tests for behavioral device simulation.""" + +import pytest +from typing import Dict, Any +from unittest.mock import Mock, MagicMock + +from src.behavioral.base import ( + BehavioralSpec, + BehavioralRegister, + BehavioralCounter, + BehaviorType +) +from src.behavioral.network_behavioral import NetworkBehavioralAnalyzer +from src.behavioral.storage_behavioral import StorageBehavioralAnalyzer +from src.behavioral.media_behavioral import MediaBehavioralAnalyzer +from src.behavioral.analyzer import BehavioralAnalyzerFactory +from src.utils.behavioral_context import build_behavioral_context + + +class TestBehavioralBase: + """Test behavioral base infrastructure.""" + + def test_behavioral_register_creation(self): + """Test creating behavioral register.""" + reg = BehavioralRegister( + name="test_reg", + offset=0x1000, + behavior=BehaviorType.CONSTANT, + default_value=0xDEADBEEF, + description="Test register" + ) + + assert reg.name == "test_reg" + assert reg.offset == 0x1000 + assert reg.behavior == BehaviorType.CONSTANT + assert reg.default_value == 0xDEADBEEF + + def test_behavioral_register_to_dict(self): + """Test converting behavioral register to dict.""" + reg = BehavioralRegister( + name="test_reg", + offset=0x1000, + behavior=BehaviorType.AUTO_INCREMENT, + default_value=0x12345678, + pattern="counter[15:0]", + counter_bits=16, + description="Test auto-increment register" + ) + + reg_dict = reg.to_dict() + assert reg_dict["offset"] == 0x1000 + assert reg_dict["behavior"] == "auto_increment" + assert reg_dict["default"] == 0x12345678 + assert reg_dict["pattern"] == "counter[15:0]" + assert reg_dict["counter_bits"] == 16 + + def test_behavioral_counter_creation(self): + """Test creating behavioral counter.""" + counter = BehavioralCounter( + name="test_counter", + width=32, + increment_rate=1, + reset_value=0, + description="Test counter" + ) + + assert counter.name == "test_counter" + assert counter.width == 32 + assert counter.increment_rate == 1 + assert counter.reset_value == 0 + + def test_behavioral_spec_creation(self): + """Test creating behavioral spec.""" + spec = BehavioralSpec("test_device") + assert spec.device_category == "test_device" + assert len(spec.registers) == 0 + assert len(spec.counters) == 0 + + def test_behavioral_spec_add_register(self): + """Test adding register to spec.""" + spec = BehavioralSpec("test_device") + + reg = BehavioralRegister( + name="test_reg", + offset=0x0000, + behavior=BehaviorType.CONSTANT, + default_value=0x00000001 + ) + + spec.add_register(reg) + assert "test_reg" in spec.registers + assert spec.registers["test_reg"].offset == 0x0000 + + def test_behavioral_spec_add_counter(self): + """Test adding counter to spec.""" + spec = BehavioralSpec("test_device") + + counter = BehavioralCounter( + name="test_counter", + width=32, + increment_rate=1 + ) + + spec.add_counter(counter) + assert "test_counter" in spec.counters + assert spec.counters["test_counter"].width == 32 + + def test_behavioral_spec_validation_success(self): + """Test behavioral spec validation succeeds with no conflicts.""" + spec = BehavioralSpec("test_device") + + # Add non-conflicting registers + spec.add_register(BehavioralRegister( + name="reg1", + offset=0x0000, + behavior=BehaviorType.CONSTANT, + default_value=0x12345678 + )) + + spec.add_register(BehavioralRegister( + name="reg2", + offset=0x0004, + behavior=BehaviorType.WRITE_CAPTURE + )) + + assert spec.validate() + + def test_behavioral_spec_validation_offset_conflict(self): + """Test detection of offset conflicts.""" + spec = BehavioralSpec("test_device") + + # Add conflicting registers + spec.add_register(BehavioralRegister( + name="reg1", + offset=0x1000, + behavior=BehaviorType.CONSTANT + )) + + spec.add_register(BehavioralRegister( + name="reg2", + offset=0x1000, # Same offset - conflict! + behavior=BehaviorType.WRITE_CAPTURE + )) + + assert not spec.validate() + + def test_behavioral_spec_to_dict(self): + """Test converting behavioral spec to dict.""" + spec = BehavioralSpec("ethernet") + + spec.add_register(BehavioralRegister( + name="link_status", + offset=0x0000, + behavior=BehaviorType.CONSTANT, + default_value=0x00000001 + )) + + spec.add_counter(BehavioralCounter( + name="rx_counter", + width=32, + increment_rate=1 + )) + + spec_dict = spec.to_dict() + assert spec_dict["device_category"] == "ethernet" + assert "link_status" in spec_dict["registers"] + assert "rx_counter" in spec_dict["counters"] + + +class TestNetworkBehavioral: + """Test network device behavioral simulation.""" + + @pytest.fixture + def network_config(self): + """Mock network device configuration.""" + config = Mock() + config.class_code = 0x020000 # Network controller + config.device_id = "1234" + config.subclass_code = 0x00 # Ethernet + return config + + def test_ethernet_behavioral_generation(self, network_config): + """Test Ethernet controller behavioral generation.""" + analyzer = NetworkBehavioralAnalyzer(network_config) + spec = analyzer.generate_spec() + + assert spec is not None + assert spec.device_category == "ethernet" + assert "link_status" in spec.registers + assert "rx_data" in spec.registers + assert "tx_data" in spec.registers + assert "mac_addr_low" in spec.registers + assert "mac_addr_high" in spec.registers + + def test_ethernet_link_status_constant(self, network_config): + """Test link status is constant and up.""" + analyzer = NetworkBehavioralAnalyzer(network_config) + spec = analyzer.generate_spec() + + link_status = spec.registers["link_status"] + assert link_status.behavior == BehaviorType.CONSTANT + assert link_status.default_value == 0x00000001 + + def test_ethernet_rx_data_auto_increment(self, network_config): + """Test RX data auto-increments.""" + analyzer = NetworkBehavioralAnalyzer(network_config) + spec = analyzer.generate_spec() + + rx_data = spec.registers["rx_data"] + assert rx_data.behavior == BehaviorType.AUTO_INCREMENT + assert rx_data.pattern is not None + assert "rx_counter" in rx_data.pattern + + def test_ethernet_tx_data_write_capture(self, network_config): + """Test TX data captures writes.""" + analyzer = NetworkBehavioralAnalyzer(network_config) + spec = analyzer.generate_spec() + + tx_data = spec.registers["tx_data"] + assert tx_data.behavior == BehaviorType.WRITE_CAPTURE + assert tx_data.default_value == 0x00000000 + + def test_ethernet_has_counters(self, network_config): + """Test Ethernet spec includes required counters.""" + analyzer = NetworkBehavioralAnalyzer(network_config) + spec = analyzer.generate_spec() + + assert "rx_counter" in spec.counters + assert "rx_packet_counter" in spec.counters + assert "tx_packet_counter" in spec.counters + + # Verify counter properties + rx_counter = spec.counters["rx_counter"] + assert rx_counter.width == 32 + assert rx_counter.increment_rate == 1 + + +class TestStorageBehavioral: + """Test storage device behavioral simulation.""" + + @pytest.fixture + def storage_config(self): + """Mock storage device configuration.""" + config = Mock() + config.class_code = 0x010802 # NVMe controller + config.device_id = "5678" + return config + + def test_nvme_behavioral_generation(self, storage_config): + """Test NVMe controller behavioral generation.""" + analyzer = StorageBehavioralAnalyzer(storage_config) + spec = analyzer.generate_spec() + + assert spec is not None + assert spec.device_category == "nvme" + assert "controller_status" in spec.registers + assert "admin_queue_attrs" in spec.registers + assert "completion_queue_head" in spec.registers + assert "submission_queue_tail" in spec.registers + + def test_nvme_controller_status_ready(self, storage_config): + """Test controller status shows ready.""" + analyzer = StorageBehavioralAnalyzer(storage_config) + spec = analyzer.generate_spec() + + status = spec.registers["controller_status"] + assert status.behavior == BehaviorType.CONSTANT + assert status.default_value == 0x00000001 + + def test_nvme_completion_queue_auto_increment(self, storage_config): + """Test completion queue head auto-increments.""" + analyzer = StorageBehavioralAnalyzer(storage_config) + spec = analyzer.generate_spec() + + cq_head = spec.registers["completion_queue_head"] + assert cq_head.behavior == BehaviorType.AUTO_INCREMENT + assert cq_head.pattern is not None + + def test_nvme_submission_queue_write_capture(self, storage_config): + """Test submission queue tail captures writes.""" + analyzer = StorageBehavioralAnalyzer(storage_config) + spec = analyzer.generate_spec() + + sq_tail = spec.registers["submission_queue_tail"] + assert sq_tail.behavior == BehaviorType.WRITE_CAPTURE + + +class TestBehavioralFactory: + """Test behavioral analyzer factory.""" + + def test_factory_network_device(self): + """Test factory creates network analyzer.""" + config = Mock() + config.class_code = 0x020000 + + analyzer = BehavioralAnalyzerFactory.create_analyzer(config) + assert analyzer is not None + assert isinstance(analyzer, NetworkBehavioralAnalyzer) + + def test_factory_storage_device(self): + """Test factory creates storage analyzer.""" + config = Mock() + config.class_code = 0x010802 + + analyzer = BehavioralAnalyzerFactory.create_analyzer(config) + assert analyzer is not None + assert isinstance(analyzer, StorageBehavioralAnalyzer) + + def test_factory_unsupported_device(self): + """Test factory returns None for unsupported device.""" + config = Mock() + config.class_code = 0xFF0000 # Unknown class + + analyzer = BehavioralAnalyzerFactory.create_analyzer(config) + assert analyzer is None + + def test_factory_generate_spec_enabled(self): + """Test factory generates spec when enabled.""" + config = Mock() + config.enable_behavioral_simulation = True + config.class_code = 0x020000 + config.device_id = "1234" + + spec = BehavioralAnalyzerFactory.generate_behavioral_spec(config) + assert spec is not None + assert spec.device_category == "ethernet" + + def test_factory_generate_spec_disabled(self): + """Test factory returns None when disabled.""" + config = Mock() + config.enable_behavioral_simulation = False + config.class_code = 0x020000 + + spec = BehavioralAnalyzerFactory.generate_behavioral_spec(config) + assert spec is None + + +class TestBehavioralContext: + """Test behavioral context integration.""" + + def test_behavioral_context_enabled(self): + """Test context generation when enabled.""" + config = Mock() + config.enable_behavioral_simulation = True + config.class_code = 0x020000 + config.device_id = "1234" + config.behavioral_bar_index = 2 + + context = build_behavioral_context(config) + + assert context.get("enable_behavioral_simulation") is True + assert "behavioral_spec" in context + assert context["behavioral_bar_index"] == 2 + + spec = context["behavioral_spec"] + assert spec["device_category"] == "ethernet" + assert "registers" in spec + assert "counters" in spec + + def test_behavioral_context_disabled(self): + """Test context contains minimal spec when disabled.""" + config = Mock() + config.enable_behavioral_simulation = False + config.class_code = 0x020000 + + context = build_behavioral_context(config) + assert context.get("enable_behavioral_simulation") is False + assert "behavioral_spec" in context + spec = context["behavioral_spec"] + assert spec["device_category"] == "generic" + assert spec["registers"] == {} + assert spec["counters"] == {} + + def test_behavioral_context_unsupported_device(self): + """Test context is empty for unsupported devices.""" + config = Mock() + config.enable_behavioral_simulation = True + config.class_code = 0xFF0000 # Unknown class + + context = build_behavioral_context(config) + assert context == {} + + def test_behavioral_context_register_details(self): + """Test context contains detailed register information.""" + config = Mock() + config.enable_behavioral_simulation = True + config.class_code = 0x020000 + config.device_id = "1234" + config.behavioral_bar_index = 0 + + context = build_behavioral_context(config) + spec = context["behavioral_spec"] + + # Check link_status register details + link_status = spec["registers"]["link_status"] + assert link_status["offset"] == 0x0000 + assert link_status["behavior"] == "constant" + assert link_status["default"] == 0x00000001 + + def test_behavioral_context_counter_details(self): + """Test context contains detailed counter information.""" + config = Mock() + config.enable_behavioral_simulation = True + config.class_code = 0x020000 + config.device_id = "1234" + config.behavioral_bar_index = 0 + + context = build_behavioral_context(config) + spec = context["behavioral_spec"] + + # Check rx_counter details + rx_counter = spec["counters"]["rx_counter"] + assert rx_counter["width"] == 32 + assert rx_counter["increment_rate"] == 1 + assert rx_counter["reset_value"] == 0 + + +class TestBehaviorTypes: + """Test behavior type enum.""" + + def test_behavior_type_values(self): + """Test behavior type enum values.""" + assert BehaviorType.CONSTANT.value == "constant" + assert BehaviorType.AUTO_INCREMENT.value == "auto_increment" + assert BehaviorType.WRITE_CAPTURE.value == "write_capture" + assert BehaviorType.RANDOM.value == "random" + assert BehaviorType.PATTERN.value == "pattern" + assert BehaviorType.TRIGGERED.value == "triggered" + assert BehaviorType.PERIODIC.value == "periodic" + + +class TestEdgeCases: + """Test edge cases and error conditions.""" + + def test_spec_with_no_registers(self): + """Test spec validation with no registers.""" + spec = BehavioralSpec("empty") + assert spec.validate() # Should still be valid + + def test_spec_with_only_counters(self): + """Test spec with counters but no registers.""" + spec = BehavioralSpec("counter_only") + spec.add_counter(BehavioralCounter("counter1", 32)) + assert spec.validate() + + def test_register_with_minimal_fields(self): + """Test register with only required fields.""" + reg = BehavioralRegister( + name="minimal", + offset=0x0000, + behavior=BehaviorType.CONSTANT + ) + assert reg.default_value == 0x00000000 + assert reg.pattern is None + assert reg.counter_bits is None + assert reg.description == "" + assert reg.read_only is False + + +class TestMediaBehavioral: + """Test media device behavioral simulation.""" + + @pytest.fixture + def audio_config(self): + """Mock audio device configuration.""" + config = Mock() + config.class_code = 0x040100 # Multimedia audio controller + config.device_id = "ABCD" + config.subclass_code = 0x01 # Audio + return config + + @pytest.fixture + def video_config(self): + """Mock video device configuration.""" + config = Mock() + config.class_code = 0x040000 # Multimedia video controller + config.device_id = "EFGH" + config.subclass_code = 0x00 # Video + return config + + @pytest.fixture + def media_config(self): + """Mock generic media device configuration.""" + config = Mock() + config.class_code = 0x040300 # Multimedia controller + config.device_id = "1357" + config.subclass_code = 0x03 # Generic + return config + + def test_audio_behavioral_generation(self, audio_config): + """Test audio controller behavioral generation.""" + analyzer = MediaBehavioralAnalyzer(audio_config) + spec = analyzer.generate_spec() + + assert spec is not None + assert spec.device_category == "audio" + assert "codec_status" in spec.registers + assert "audio_stream_status" in spec.registers + assert "audio_buffer_position" in spec.registers + # Should NOT have video registers + assert "video_frame_counter" not in spec.registers + + def test_video_behavioral_generation(self, video_config): + """Test video controller behavioral generation.""" + analyzer = MediaBehavioralAnalyzer(video_config) + spec = analyzer.generate_spec() + + assert spec is not None + assert spec.device_category == "video" + assert "codec_status" in spec.registers + assert "video_stream_status" in spec.registers + assert "video_frame_counter" in spec.registers + # Should NOT have audio registers + assert "audio_buffer_position" not in spec.registers + + def test_generic_media_behavioral_generation(self, media_config): + """Test generic media controller behavioral generation.""" + analyzer = MediaBehavioralAnalyzer(media_config) + spec = analyzer.generate_spec() + + assert spec is not None + assert spec.device_category == "media" + assert "codec_status" in spec.registers + + def test_media_codec_status_ready(self, audio_config): + """Test codec status shows ready.""" + analyzer = MediaBehavioralAnalyzer(audio_config) + spec = analyzer.generate_spec() + + codec_status = spec.registers["codec_status"] + assert codec_status.behavior == BehaviorType.CONSTANT + assert codec_status.default_value == 0x00000003 # Ready + initialized + + def test_media_audio_buffer_auto_increment(self, audio_config): + """Test audio buffer position auto-increments.""" + analyzer = MediaBehavioralAnalyzer(audio_config) + spec = analyzer.generate_spec() + + audio_pos = spec.registers["audio_buffer_position"] + assert audio_pos.behavior == BehaviorType.AUTO_INCREMENT + assert audio_pos.pattern is not None + assert "audio_position_counter" in audio_pos.pattern + + def test_media_video_frame_counter(self, video_config): + """Test video frame counter auto-increments.""" + analyzer = MediaBehavioralAnalyzer(video_config) + spec = analyzer.generate_spec() + + frame_counter = spec.registers["video_frame_counter"] + assert frame_counter.behavior == BehaviorType.AUTO_INCREMENT + assert frame_counter.pattern is not None + + def test_media_dma_status_ready(self, audio_config): + """Test DMA status shows ready.""" + analyzer = MediaBehavioralAnalyzer(audio_config) + spec = analyzer.generate_spec() + + dma_status = spec.registers["dma_status"] + assert dma_status.behavior == BehaviorType.CONSTANT + assert dma_status.default_value == 0x00000001 + + def test_media_writable_registers(self, audio_config): + """Test writable control registers.""" + analyzer = MediaBehavioralAnalyzer(audio_config) + spec = analyzer.generate_spec() + + # Interrupt control should be writable + int_status = spec.registers["interrupt_status"] + assert int_status.behavior == BehaviorType.WRITE_CAPTURE + + # Volume control should be writable + volume = spec.registers["volume_control"] + assert volume.behavior == BehaviorType.WRITE_CAPTURE + assert volume.default_value == 0x00008080 # 50% volume + + def test_media_has_counters(self, audio_config): + """Test media spec includes required counters.""" + analyzer = MediaBehavioralAnalyzer(audio_config) + spec = analyzer.generate_spec() + + assert "audio_position_counter" in spec.counters + assert "dma_buffer_counter" in spec.counters + assert "buffer_level_counter" in spec.counters + assert "frames_processed_counter" in spec.counters + assert "bytes_transferred_counter" in spec.counters + + # Verify counter properties + audio_counter = spec.counters["audio_position_counter"] + assert audio_counter.width == 16 + assert audio_counter.increment_rate == 64 # Sample block size + + def test_media_resolution_register(self, video_config): + """Test video resolution register.""" + analyzer = MediaBehavioralAnalyzer(video_config) + spec = analyzer.generate_spec() + + resolution = spec.registers["video_resolution"] + assert resolution.behavior == BehaviorType.CONSTANT + assert resolution.default_value == 0x07800438 # 1920x1080 + + def test_media_sample_rate_register(self, audio_config): + """Test audio sample rate register.""" + analyzer = MediaBehavioralAnalyzer(audio_config) + spec = analyzer.generate_spec() + + sample_rate = spec.registers["audio_sample_rate"] + assert sample_rate.behavior == BehaviorType.CONSTANT + assert sample_rate.default_value == 0x0000AC44 # 44.1 kHz + + def test_factory_media_device(self): + """Test factory creates media analyzer.""" + config = Mock() + config.class_code = 0x040100 # Multimedia audio + + analyzer = BehavioralAnalyzerFactory.create_analyzer(config) + assert analyzer is not None + assert isinstance(analyzer, MediaBehavioralAnalyzer) + + def test_media_context_generation(self): + """Test media behavioral context generation.""" + config = Mock() + config.enable_behavioral_simulation = True + config.class_code = 0x040100 # Multimedia audio + config.device_id = "ABCD" + config.subclass_code = 0x01 + config.behavioral_bar_index = 0 + + context = build_behavioral_context(config) + + assert context.get("enable_behavioral_simulation") is True + assert "behavioral_spec" in context + + spec = context["behavioral_spec"] + assert spec["device_category"] == "audio" + assert "codec_status" in spec["registers"] + assert "audio_position_counter" in spec["counters"] + diff --git a/tests/test_cfg_shadow_template.py b/tests/test_cfg_shadow_template.py index c95614b5..a2b6f018 100644 --- a/tests/test_cfg_shadow_template.py +++ b/tests/test_cfg_shadow_template.py @@ -512,7 +512,16 @@ def test_synthesis_attributes_included(self, template_content, minimal_context): # Check for synthesis attributes assert '(* ram_style="block" *)' in result - assert "(* ram_init_file = CFG_INIT_HEX *)" in result + + # CRITICAL REGRESSION TEST: ram_init_file attribute must NOT be present + # String parameters cannot be used in synthesis attributes in Vivado + # This would cause: ERROR: [Synth 8-281] expression must be of a packed type + assert "(* ram_init_file = CFG_INIT_HEX *)" not in result, \ + "ram_init_file attribute with string parameter detected - this causes Vivado synthesis error" + + # Instead, initialization should be done via $readmemh in initial block + assert "$readmemh(CFG_INIT_HEX, config_space_ram)" in result, \ + "BRAM initialization should use $readmemh in initial block, not synthesis attribute" # Check for synthesis pragmas assert "// synthesis translate_off" in result @@ -560,6 +569,130 @@ def test_get_effective_reg_num_function(self, template_content, minimal_context) assert "cfg_a7[1] ? cfg_a7[0] : reg_num[7]" in result self.validate_systemverilog_syntax(result) + + # ========================================================================== + # Vivado Synthesis Compatibility Tests (Regression Prevention) + # ========================================================================== + + def test_vivado_synthesis_attribute_compatibility(self, template_content, minimal_context): + """Test that generated SystemVerilog is compatible with Vivado synthesis requirements. + + This test prevents regressions of known Vivado synthesis errors: + - ERROR: [Synth 8-281] expression must be of a packed type + - Invalid ram_style attribute values + - String parameters in synthesis attributes + """ + result = self.render_template(template_content, minimal_context) + + # TEST 1: No string parameters in synthesis attributes + # String parameters like CFG_INIT_HEX cannot be used in (* attribute = value *) syntax + # They must use procedural blocks like $readmemh instead + attribute_pattern = r'\(\*\s*\w+\s*=\s*[A-Z_]+\s*\*\)' + matches = re.findall(attribute_pattern, result) + for match in matches: + # Extract the value part + value_match = re.search(r'=\s*([A-Z_]+)', match) + if value_match: + attr_value = value_match.group(1) + # Check if it looks like a string parameter (all caps, underscores) + if attr_value.isupper() and '_' in attr_value: + # Make sure it's not a valid packed constant + assert attr_value not in ['CFG_INIT_HEX'], \ + f"String parameter {attr_value} found in synthesis attribute: {match}" + + # TEST 2: ram_style attributes must use valid string literals + ram_style_pattern = r'\(\*\s*ram_style\s*=\s*"([^"]+)"\s*\*\)' + ram_styles = re.findall(ram_style_pattern, result) + valid_ram_styles = ['block', 'distributed', 'registers', 'ultra', 'auto'] + for ram_style in ram_styles: + assert ram_style in valid_ram_styles, \ + f"Invalid ram_style value: {ram_style}. Must be one of: {valid_ram_styles}" + + # TEST 3: ram_style applied to unpacked arrays must be single logic type + # Pattern: (* ram_style="..." *) logic [...] array_name[...]; + ram_style_unpacked = re.findall( + r'\(\*\s*ram_style\s*=\s*"[^"]+"\s*\*\)\s*logic\s+(?:\[[^\]]+\])?\s*(\w+)\[[^\]]+\]', + result + ) + # All these should be simple unpacked arrays, not structs + for array_name in ram_style_unpacked: + # Make sure we don't have struct types (would cause "expression must be packed" error) + assert not re.search( + rf'typedef\s+struct.*{array_name}', + result + ), f"ram_style attribute on struct array {array_name} not allowed" + + # TEST 4: Verify $readmemh is used for BRAM initialization instead of attributes + if 'CFG_INIT_HEX' in result: + assert '$readmemh(CFG_INIT_HEX, config_space_ram)' in result, \ + "BRAM initialization must use $readmemh, not synthesis attributes" + + # TEST 5: No ram_style with invalid bit-width values + # Pattern that would cause issues: (* ram_style="88'b000..." *) + invalid_ram_style_pattern = r'\(\*\s*ram_style\s*=\s*"\d+\'[bo][01]+"\s*\*\)' + invalid_matches = re.findall(invalid_ram_style_pattern, result) + assert len(invalid_matches) == 0, \ + f"Found invalid ram_style with bit literal: {invalid_matches}" + + self.validate_systemverilog_syntax(result) + + def test_no_unpacked_type_in_attributes(self, template_content, minimal_context): + """Ensure synthesis attributes are never applied to unpacked types or structs. + + Vivado requires attributes on arrays to be applied to packed types only. + This test catches: ERROR: [Synth 8-281] expression must be of a packed type + """ + result = self.render_template(template_content, minimal_context) + + # Find all synthesis attributes + attr_pattern = r'\(\*[^)]+\*\)\s*(?:logic|reg|wire)?\s*(?:\[[^\]]+\])?\s*(\w+)' + + # Check hash table declarations specifically (they were problematic) + hash_table_attrs = re.findall( + r'\(\*\s*ram_style\s*=\s*"([^"]+)"\s*\*\)\s*logic\s+(?:\[[^\]]+\])?\s*(hash_table_\w+)\[', + result + ) + + for ram_style, var_name in hash_table_attrs: + # Verify the ram_style is a valid string literal, not a bit pattern + assert ram_style in ['block', 'distributed', 'registers', 'ultra', 'auto'], \ + f"Invalid ram_style '{ram_style}' on {var_name}" + + # Make sure it's not malformed like "88'b000..." + assert not re.match(r'\d+\'[bo]', ram_style), \ + f"Malformed ram_style on {var_name}: {ram_style}" + + def test_bram_initialization_via_readmemh(self, template_content, minimal_context): + """Verify BRAM initialization uses $readmemh instead of synthesis attributes. + + String parameters cannot be used in synthesis attributes, so initialization + must be done via procedural code in an initial block. + """ + result = self.render_template(template_content, minimal_context) + + # Check that config_space_ram is declared without ram_init_file attribute + config_ram_decl = re.search( + r'logic\s+\[31:0\]\s+config_space_ram\[0:[^\]]+\];', + result + ) + assert config_ram_decl, "config_space_ram declaration not found" + + # Get the lines around the declaration + decl_pos = config_ram_decl.start() + context_start = max(0, decl_pos - 200) + context_end = min(len(result), decl_pos + 200) + context = result[context_start:context_end] + + # Ensure NO ram_init_file attribute near the declaration + assert 'ram_init_file' not in context, \ + "ram_init_file attribute should not be used with string parameters" + + # Verify $readmemh is used in initial block instead + assert re.search( + r'if\s*\(\s*CFG_INIT_HEX\s*!=\s*""\s*\)\s*begin\s+\$readmemh\(\s*CFG_INIT_HEX\s*,\s*config_space_ram\s*\)', + result, + re.MULTILINE + ), "BRAM initialization must use $readmemh in initial block" if __name__ == "__main__": diff --git a/tests/test_constraints_template_defaults.py b/tests/test_constraints_template_defaults.py index 3bbf49a8..0a938530 100644 --- a/tests/test_constraints_template_defaults.py +++ b/tests/test_constraints_template_defaults.py @@ -26,6 +26,10 @@ def test_constraints_renders_without_constraint_files(self): ), # Header is required by TCL templates "header": generate_tcl_header_comment("TCL Constraints"), + # PCIe clock parameters (added for 7-series support) + "pcie_refclk_freq": 0, # 0=100MHz + "pcie_userclk1_freq": 2, # 2=62.5MHz + "pcie_userclk2_freq": 2, # 2=62.5MHz # Intentionally omit 'constraint_files' to verify default injection # Other optional keys like sys_clk_freq_mhz have in-template defaults } diff --git a/tests/test_oui_dsn_extraction.py b/tests/test_oui_dsn_extraction.py new file mode 100644 index 00000000..7b7fa268 --- /dev/null +++ b/tests/test_oui_dsn_extraction.py @@ -0,0 +1,303 @@ +#!/usr/bin/env python3 +""" +Unit tests for OUI extraction and DSN semantic decomposition. + +Tests verify that: +1. OUI is correctly extracted from vendor IDs +2. DSN is properly decomposed into semantic components +3. Template context contains all required fields for SystemVerilog defines +""" + + +import logging +import sys +from pathlib import Path +from typing import Any, Dict + +import pytest + +# Add src to path for imports +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from src.string_utils import log_info_safe, safe_format +from src.templating.sv_context_builder import SVContextBuilder + + + +class TestOUIExtraction: + """Test suite for OUI extraction from vendor IDs.""" + + @pytest.fixture + def context_builder(self): + """Provide SVContextBuilder instance.""" + logger = logging.getLogger("test_oui") + return SVContextBuilder(logger) + + def test_oui_extraction_from_vendor_id(self, context_builder): + """Test OUI extraction from vendor ID.""" + context = {} + vendor_id_int = 0x10DE # NVIDIA + + context_builder._add_vendor_oui(context, vendor_id_int) + + assert "vendor_oui" in context + assert context["vendor_oui"] == 0x10DE + assert "vendor_oui_hex" in context + assert context["vendor_oui_hex"] == "0x0010DE" + assert "pci_exp_ep_oui" in context + assert context["pci_exp_ep_oui"] == 0x10DE + + def test_oui_extraction_24bit_mask(self, context_builder): + """Test OUI is properly masked to 24 bits.""" + context = {} + vendor_id_int = 0xFF10DE # Value with upper bits set + + context_builder._add_vendor_oui(context, vendor_id_int) + + # Should mask to 24 bits (0xFFFFFF & 0xFF10DE = 0xFF10DE) + assert context["vendor_oui"] == 0xFF10DE + assert context["vendor_oui_hex"] == "0xFF10DE" + + def test_oui_extraction_zero(self, context_builder): + """Test OUI extraction with zero vendor ID.""" + context = {} + vendor_id_int = 0x0000 + + context_builder._add_vendor_oui(context, vendor_id_int) + + assert context["vendor_oui"] == 0x0000 + assert context["vendor_oui_hex"] == "0x000000" + + def test_oui_extraction_max_value(self, context_builder): + """Test OUI extraction with maximum 24-bit value.""" + context = {} + vendor_id_int = 0xFFFFFF + + context_builder._add_vendor_oui(context, vendor_id_int) + + assert context["vendor_oui"] == 0xFFFFFF + assert context["vendor_oui_hex"] == "0xFFFFFF" + + +class TestDSNSemanticDecomposition: + """Test suite for DSN semantic decomposition.""" + + @pytest.fixture + def context_builder(self): + """Provide SVContextBuilder instance.""" + logger = logging.getLogger("test_dsn") + return SVContextBuilder(logger) + + def test_dsn_decomposition_basic(self, context_builder): + """Test basic DSN decomposition into upper/lower 32-bit parts.""" + context = {} + # Example DSN: upper 32 bits = 0x00000001, lower 32 bits = 0x01000A35 + dsn_value = 0x0000000101000A35 + + context_builder._add_dsn_semantic_fields(context, dsn_value) + + assert context["dsn_upper_32"] == 0x00000001 + assert context["dsn_lower_32"] == 0x01000A35 + assert context["pci_exp_ep_dsn_2"] == 0x00000001 + assert context["pci_exp_ep_dsn_1"] == 0x01000A35 + + def test_dsn_oui_extraction(self, context_builder): + """Test OUI extraction from DSN lower 32 bits.""" + context = {} + # DSN with OUI 0x000A35 in lower bits + dsn_value = 0x0000000101000A35 + + context_builder._add_dsn_semantic_fields(context, dsn_value) + + assert context["dsn_oui"] == 0x000A35 + assert context["dsn_oui_hex"] == "0x000A35" + + def test_dsn_extension_extraction(self, context_builder): + """Test extension byte extraction from DSN lower 32 bits.""" + context = {} + # DSN with extension byte 0x01 in bits [31:24] of lower 32 bits + dsn_value = 0x0000000101000A35 + + context_builder._add_dsn_semantic_fields(context, dsn_value) + + assert context["dsn_extension"] == 0x01 + assert context["dsn_extension_hex"] == "0x01" + + def test_dsn_hex_format_strings(self, context_builder): + """Test DSN hex format strings for SystemVerilog defines.""" + context = {} + dsn_value = 0x123456789ABCDEF0 + + context_builder._add_dsn_semantic_fields(context, dsn_value) + + # Check SystemVerilog format strings + assert context["pci_exp_ep_dsn_2_hex"] == "32'h12345678" + assert context["pci_exp_ep_dsn_1_hex"] == "32'h9ABCDEF0" + + def test_dsn_zero_value(self, context_builder): + """Test DSN decomposition with zero value.""" + context = {} + dsn_value = 0x0000000000000000 + + context_builder._add_dsn_semantic_fields(context, dsn_value) + + assert context["dsn_upper_32"] == 0x00000000 + assert context["dsn_lower_32"] == 0x00000000 + assert context["dsn_oui"] == 0x000000 + assert context["dsn_extension"] == 0x00 + + def test_dsn_max_value(self, context_builder): + """Test DSN decomposition with maximum value.""" + context = {} + dsn_value = 0xFFFFFFFFFFFFFFFF + + context_builder._add_dsn_semantic_fields(context, dsn_value) + + assert context["dsn_upper_32"] == 0xFFFFFFFF + assert context["dsn_lower_32"] == 0xFFFFFFFF + assert context["dsn_oui"] == 0xFFFFFF + assert context["dsn_extension"] == 0xFF + + +class TestIntegratedContextBuilding: + """Test integrated context building with OUI and DSN fields.""" + + @pytest.fixture + def context_builder(self): + """Provide SVContextBuilder instance.""" + logger = logging.getLogger("test_integrated") + return SVContextBuilder(logger) + + @pytest.fixture + def mock_template_context(self) -> Dict[str, Any]: + """Provide mock template context with device data.""" + return { + "device_config": { + "vendor_id": "0x10DE", + "device_id": "0x1234", + }, + "device_serial_number": 0x0000000101000A35, + "device_signature": "10de:1234", # Required field + "generation_metadata": { + "version": "1.0", + }, + } + + def test_full_context_with_oui_and_dsn( + self, context_builder, mock_template_context + ): + """Test that full context contains OUI and DSN fields.""" + # Mock the power/error/perf configs + mock_power_config = type("obj", (object,), {})() + mock_error_config = type("obj", (object,), {})() + mock_perf_config = type("obj", (object,), {})() + mock_device_config = type("obj", (object,), {})() + + # Build context + context = context_builder.build_enhanced_context( + mock_template_context, + mock_power_config, + mock_error_config, + mock_perf_config, + mock_device_config, + ) + + # Verify OUI fields + assert "vendor_oui" in context + assert "vendor_oui_hex" in context + assert "pci_exp_ep_oui" in context + + # Verify DSN semantic fields + assert "dsn_upper_32" in context + assert "dsn_lower_32" in context + assert "dsn_oui" in context + assert "dsn_extension" in context + + # Verify SystemVerilog define fields + assert "pci_exp_ep_dsn_2" in context + assert "pci_exp_ep_dsn_1" in context + assert "pci_exp_ep_dsn_2_hex" in context + assert "pci_exp_ep_dsn_1_hex" in context + + def test_context_values_match_input( + self, context_builder, mock_template_context + ): + """Test that context values match input data.""" + # Mock configs + mock_power_config = type("obj", (object,), {})() + mock_error_config = type("obj", (object,), {})() + mock_perf_config = type("obj", (object,), {})() + mock_device_config = type("obj", (object,), {})() + + # Build context + context = context_builder.build_enhanced_context( + mock_template_context, + mock_power_config, + mock_error_config, + mock_perf_config, + mock_device_config, + ) + + # Verify vendor OUI matches vendor ID + assert context["vendor_oui"] == 0x10DE + + # Verify DSN decomposition + assert context["device_serial_number_int"] == 0x0000000101000A35 + assert context["dsn_upper_32"] == 0x00000001 + assert context["dsn_lower_32"] == 0x01000A35 + assert context["dsn_oui"] == 0x000A35 + + +class TestTemplateCompatibility: + """Test that new fields are compatible with templates.""" + + @pytest.fixture + def context_builder(self): + """Provide SVContextBuilder instance.""" + logger = logging.getLogger("test_template_compat") + return SVContextBuilder(logger) + + def test_all_required_fields_present(self, context_builder): + """Test that all required fields for templates are present.""" + context = {} + + # Add OUI + context_builder._add_vendor_oui(context, 0x10DE) + + # Add DSN semantic fields + context_builder._add_dsn_semantic_fields(context, 0x0000000101000A35) + + # Required fields for pcie_endpoint_defines.sv.j2 + required_fields = [ + "vendor_oui", + "vendor_oui_hex", + "pci_exp_ep_oui", + "dsn_upper_32", + "dsn_lower_32", + "dsn_oui", + "dsn_oui_hex", + "dsn_extension", + "dsn_extension_hex", + "pci_exp_ep_dsn_2", + "pci_exp_ep_dsn_1", + "pci_exp_ep_dsn_2_hex", + "pci_exp_ep_dsn_1_hex", + ] + + for field in required_fields: + assert field in context, f"Missing required field: {field}" + + def test_format_strings_are_valid_systemverilog(self, context_builder): + """Test that format strings are valid SystemVerilog syntax.""" + context = {} + context_builder._add_dsn_semantic_fields(context, 0x123456789ABCDEF0) + + # Check that hex format strings match SystemVerilog syntax + assert context["pci_exp_ep_dsn_2_hex"].startswith("32'h") + assert context["pci_exp_ep_dsn_1_hex"].startswith("32'h") + assert len(context["pci_exp_ep_dsn_2_hex"]) == 12 # "32'h" + 8 hex digits + assert len(context["pci_exp_ep_dsn_1_hex"]) == 12 + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/tests/test_top_level_wrapper_template.py b/tests/test_top_level_wrapper_template.py index bb930c16..6e516fd1 100644 --- a/tests/test_top_level_wrapper_template.py +++ b/tests/test_top_level_wrapper_template.py @@ -195,7 +195,9 @@ def test_cfg_dsn_and_mgmt_tieoffs(self, template_env, base_context): "localparam logic [63:0] DEVICE_SERIAL_NUMBER = 64'hAABBCCDDEEFF0011;" in result ) - assert "assign cfg_mgmt_wr_rw1c_as_rw = 1'b1;" in result + # cfg_mgmt_wr_rw1c_as_rw can be tied off with wire declaration or assign + assert ("wire cfg_mgmt_wr_rw1c_as_rw = 1'b1;" in result or + "assign cfg_mgmt_wr_rw1c_as_rw = 1'b1;" in result) assert "assign cfg_dsn = DEVICE_SERIAL_NUMBER;" in result assert ".cfg_mgmt_wr_rw1c_as_rw(cfg_mgmt_wr_rw1c_as_rw)" in result assert ".cfg_dsn(cfg_dsn)" in result