├── mmio ├── __init__.py ├── config │ ├── __init__.py │ ├── base_config.py │ ├── cli_config.py │ ├── verilog_settings.py │ ├── mmio_settings.py │ └── log_settings.py ├── core │ ├── __init__.py │ ├── exceptions.py │ ├── mmio_parser.py │ ├── logger.py │ └── parse_logic.py ├── domain │ ├── __init__.py │ ├── models │ │ ├── __init__.py │ │ └── verilog_data.py │ └── services │ │ ├── __init__.py │ │ └── orchestrators │ │ ├── __init__.py │ │ ├── output_orchestrator.py │ │ ├── input_orchestrator.py │ │ └── main_orchestrator.py ├── application │ ├── cli │ │ ├── __init__.py │ │ ├── commands │ │ │ ├── __init__.py │ │ │ ├── file_select.py │ │ │ ├── output_select.py │ │ │ ├── log_settings.py │ │ │ └── mmio_select.py │ │ ├── coordinator │ │ │ ├── __init__.py │ │ │ └── cli_coordinator.py │ │ ├── mmio.py │ │ └── app.py │ ├── verilog │ │ ├── __init__.py │ │ ├── generators │ │ │ ├── __init__.py │ │ │ ├── address_check.py │ │ │ ├── counter_generator.py │ │ │ ├── response_logic.py │ │ │ ├── static_generator.py │ │ │ └── rom.py │ │ ├── verilog_models.py │ │ └── verilog_builder_orchestrator.py │ └── __init__.py ├── infrastructure │ ├── __init__.py │ ├── cli │ │ ├── __init__.py │ │ └── base.py │ └── file_handling │ │ ├── __init__.py │ │ ├── mmio_filemanager.py │ │ ├── verilog_filemanager.py │ │ ├── base_file_manager.py │ │ ├── base_output_manager.py │ │ └── base_input_manager.py └── __main__.py ├── output └── verilog │ └── .gitkeep ├── .env.example ├── setup.py ├── run.bat ├── input └── mmio │ └── test.trace └── README.md /mmio/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mmio/config/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mmio/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mmio/domain/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /output/verilog/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mmio/application/cli/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mmio/domain/models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mmio/domain/services/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mmio/infrastructure/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mmio/application/verilog/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mmio/infrastructure/cli/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mmio/application/cli/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mmio/application/cli/coordinator/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mmio/domain/services/orchestrators/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mmio/infrastructure/file_handling/__init__.py: -------------------------------------------------------------------------------- 1 | """File handling infrastructure for MMIO.""" 2 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Name of the Verilog module 2 | MODULE_HEADER=cool_bar_controller 3 | 4 | MMIO_FILE_INPUT_PATH= 5 | 6 | VERILOG_FILE_OUTPUT_PATH= 7 | 8 | LOG_FILE_OUTPUT_PATH= 9 | 10 | LOG_SETTINGS_FILE= 11 | -------------------------------------------------------------------------------- /mmio/application/__init__.py: -------------------------------------------------------------------------------- 1 | """CLI App.""" 2 | 3 | from mmio.application.cli.app import AppLogic 4 | from mmio.application.cli.commands.mmio_select import SelectMMIOFileInputCLI 5 | from mmio.application.cli.commands.output_select import OutputVerilogCLI 6 | from mmio.application.cli.mmio import MMIOCLIManager 7 | 8 | __all__ = [ 9 | "AppLogic", 10 | "SelectMMIOFileInputCLI", 11 | "OutputVerilogCLI", 12 | "MMIOCLIManager", 13 | ] 14 | -------------------------------------------------------------------------------- /mmio/application/verilog/generators/__init__.py: -------------------------------------------------------------------------------- 1 | from mmio.application.verilog.generators.address_check import AddressCheckGenerator 2 | from mmio.application.verilog.generators.counter_generator import CounterGenerator 3 | from mmio.application.verilog.generators.response_logic import ResponseLogicGenerator 4 | from mmio.application.verilog.generators.rom import ROMGenerator 5 | from mmio.application.verilog.generators.static_generator import StaticCodeGenerator 6 | 7 | __all__: list[str] = [ 8 | "AddressCheckGenerator", 9 | "CounterGenerator", 10 | "ROMGenerator", 11 | "StaticCodeGenerator", 12 | "ResponseLogicGenerator", 13 | ] 14 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Create the initial package directory structure.""" 2 | 3 | from setuptools import find_packages, setup 4 | 5 | setup( 6 | name="mmio_parser", 7 | version="0.1.0", 8 | packages=find_packages(), 9 | install_requires=[ 10 | "pydantic>=2.6.0", 11 | "pydantic-settings>=2.7.1", 12 | "click>=8.0.0", 13 | "rich>=13.9.4", 14 | ], 15 | extras_require={ 16 | "dev": [ 17 | "mypy>=1.14.1", 18 | "pyright>=1.1.392.post0", 19 | "pylance>=0.22.0", 20 | "ruff>=0.9.2", 21 | "pre-commit>=3.6.0", 22 | "pytest>=8.3.4", 23 | ] 24 | }, 25 | python_requires=">=3.10", 26 | include_package_data=True, 27 | data_files=[("input/mmio", []), ("output/verilog", [])], 28 | ) 29 | -------------------------------------------------------------------------------- /mmio/domain/services/orchestrators/output_orchestrator.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | 3 | from mmio.core.logger import LoggerManager 4 | from mmio.infrastructure.file_handling.verilog_filemanager import VerilogFileManager 5 | 6 | logger = LoggerManager.get_logger(__name__) 7 | 8 | 9 | class OutputOrchestrator(BaseModel): 10 | """Handle output operations. 11 | 12 | - Accepts generated verilog code. 13 | - Uses VerilogFileManager to write the code to disk. 14 | """ 15 | 16 | verilog_file_manager: VerilogFileManager = Field(default_factory=VerilogFileManager.get_instance) 17 | 18 | def output(self, verilog_code: str) -> None: 19 | """Write the generated verilog code to a file. 20 | 21 | Args: 22 | verilog_code (str): The verilog code to be written. 23 | 24 | """ 25 | if not self.verilog_file_manager.output_manager.file_manager.file_name: 26 | self.verilog_file_manager.generate_output_filename() 27 | 28 | self.verilog_file_manager.write_file(verilog_code) 29 | -------------------------------------------------------------------------------- /mmio/config/base_config.py: -------------------------------------------------------------------------------- 1 | """Base configuration class.""" 2 | 3 | from pathlib import Path 4 | from typing import ClassVar, Optional 5 | 6 | from pydantic import Field 7 | from pydantic_settings import BaseSettings, SettingsConfigDict 8 | 9 | 10 | class BaseConfig(BaseSettings): 11 | """Base configuration class.""" 12 | 13 | _instance: ClassVar[Optional["BaseConfig"]] = None 14 | 15 | workdir: Path = Field(default=(Path.cwd())) 16 | format: str = Field(default="%(asctime)s - %(name)s - %(levelname)s - %(message)s") 17 | date_format: str = Field(default="%Y-%m-%d %H:%M:%S") 18 | 19 | model_config = SettingsConfigDict( 20 | env_file=".env", 21 | env_file_encoding="utf-8", 22 | case_sensitive=False, 23 | extra="allow", 24 | env_ignore_empty=True, 25 | env_prefix="BASE_", 26 | ) 27 | 28 | @classmethod 29 | def get_instance(cls) -> "BaseConfig": 30 | """Get the singleton instance of BaseConfig.""" 31 | if cls._instance is None: 32 | cls._instance = cls() 33 | return cls._instance 34 | 35 | 36 | base_config_instance = BaseConfig() 37 | base_config: BaseConfig = BaseConfig.get_instance() 38 | -------------------------------------------------------------------------------- /mmio/__main__.py: -------------------------------------------------------------------------------- 1 | """Main entry point for the MMIO package.""" 2 | 3 | from mmio.config.log_settings import LogSettings 4 | from mmio.core.logger import LoggerManager 5 | from mmio.domain.services.orchestrators.main_orchestrator import MainOrchestrator 6 | 7 | log_settings = LogSettings.load() 8 | log_settings.apply() 9 | logger = LoggerManager.get_logger(__name__) 10 | 11 | 12 | def main() -> None: 13 | """Entry point for the MMIO package. 14 | 15 | This function initializes the application components and starts 16 | the main orchestration flow. 17 | """ 18 | try: 19 | logger.info("Starting MMIO application") 20 | logger.info("Creating MainOrchestrator instance") 21 | 22 | orchestrator = MainOrchestrator() 23 | logger.info("MainOrchestrator instance created") 24 | 25 | logger.info("Initializing orchestrator") 26 | orchestrator.initialize() 27 | logger.info("Orchestrator initialization completed") 28 | 29 | logger.info("Starting main orchestration flow") 30 | orchestrator.run() 31 | logger.info("Main orchestration flow completed") 32 | 33 | logger.info("MMIO application completed successfully") 34 | except Exception as e: 35 | logger.error(f"Application failed: {e}", exc_info=True) 36 | raise 37 | 38 | 39 | if __name__ == "__main__": 40 | main() 41 | -------------------------------------------------------------------------------- /mmio/infrastructure/file_handling/mmio_filemanager.py: -------------------------------------------------------------------------------- 1 | """MMIO File Manager for handling MMIO log files.""" 2 | 3 | from pathlib import Path 4 | from typing import ClassVar, Optional 5 | 6 | from pydantic import BaseModel, Field 7 | 8 | from mmio.core.logger import LoggerManager 9 | from mmio.infrastructure.file_handling.base_input_manager import InputManager 10 | 11 | logger = LoggerManager.get_logger(__name__) 12 | 13 | 14 | class MMIOFileManager(BaseModel): 15 | """File Manager to handle MMIO log file operations. 16 | 17 | Specialized manager for handling MMIO log files input operations. 18 | Uses composition with InputManager for file operations. 19 | 20 | Attributes: 21 | input_manager: Input manager instance for file operations 22 | 23 | """ 24 | 25 | _instance: ClassVar[Optional["MMIOFileManager"]] = None 26 | input_manager: InputManager = Field(default_factory=InputManager.get_instance) 27 | 28 | @classmethod 29 | def get_instance(cls) -> "MMIOFileManager": 30 | """Get the singleton instance of MMIOFileManager.""" 31 | if cls._instance is None: 32 | cls._instance = cls() 33 | return cls._instance 34 | 35 | def read_file(self) -> Path: 36 | """Validate and return the path to the specified MMIO log file.""" 37 | return self.input_manager.read_file() 38 | 39 | def list_files(self) -> list[Path]: 40 | """List all files in the input directory.""" 41 | return self.input_manager.list_files() 42 | -------------------------------------------------------------------------------- /mmio/core/exceptions.py: -------------------------------------------------------------------------------- 1 | """Custom exceptions for MMIO parser error handling. 2 | 3 | This module provides a comprehensive set of exceptions for handling various 4 | error scenarios in MMIO parsing operations. 5 | """ 6 | 7 | from mmio.core.logger import LoggerManager 8 | 9 | logger = LoggerManager.get_logger(__name__) 10 | 11 | 12 | class MMIOParserError(Exception): 13 | """Base exception class for MMIO parser errors. 14 | 15 | All other MMIO-related exceptions should inherit from this class. 16 | """ 17 | 18 | def __init__(self, message: str, line_number: int | None = None) -> None: 19 | """Initialize the error with an optional line number. 20 | 21 | Args: 22 | message: Detailed error description 23 | line_number: Optional line number where the error occurred 24 | 25 | """ 26 | self.line_number = line_number 27 | super().__init__(f"Line {line_number}: {message}" if line_number else message) 28 | 29 | 30 | class ValidationError(MMIOParserError): 31 | """Raised when MMIO data validation fails. 32 | 33 | Examples: 34 | - Invalid address format 35 | - Invalid operation type 36 | - Out of range values 37 | 38 | """ 39 | 40 | pass 41 | 42 | 43 | class FileAccessError(MMIOParserError): 44 | """Raised when there are issues accessing MMIO files. 45 | 46 | Examples: 47 | - File not found 48 | - Permission denied 49 | - File is empty 50 | 51 | """ 52 | 53 | pass 54 | -------------------------------------------------------------------------------- /mmio/config/cli_config.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, ConfigDict, Field 2 | 3 | from mmio.core.logger import LoggerManager 4 | 5 | logger = LoggerManager.get_logger(__name__) 6 | 7 | 8 | class CLIOptions(BaseModel): 9 | """CLI Options for controlling the Verilog generation process. 10 | 11 | Attributes: 12 | bar_selection: Optional list of BAR numbers to process; if None, process all. 13 | operation_filter: Either "R" (read only), "W" (write only), or "B" (both). 14 | include_address_checks: Whether to generate address check entries. 15 | include_counters: Whether to generate counter entries. 16 | include_default_values: Whether to calculate and include default values. 17 | include_logic: Whether to include logic structures. 18 | include_state_machines: Whether to include state machine structures. 19 | 20 | """ 21 | 22 | model_config = ConfigDict(arbitrary_types_allowed=True) 23 | 24 | bar_selection: list[int] | None = Field(default=None, description="List of BARs to process") 25 | operation_filter: str = Field(default="B", description="Operation filter (R/W/B)") 26 | include_address_checks: bool = Field(default=True, description="Whether to include address check structures") 27 | include_counters: bool = Field(default=True, description="Whether to include counter structures") 28 | include_default_values: bool = Field(default=True, description="Whether to include default value structures") 29 | include_logic: bool = Field(default=True, description="Whether to include logic structures") 30 | include_state_machines: bool = Field(default=True, description="Whether to include state machine structures") 31 | init_roms: bool = Field(default=True, description="Whether to include ROM initialization structures") 32 | -------------------------------------------------------------------------------- /mmio/infrastructure/file_handling/verilog_filemanager.py: -------------------------------------------------------------------------------- 1 | """Verilog File Manager for handling Verilog file generation.""" 2 | 3 | from datetime import datetime 4 | from pathlib import Path 5 | from typing import ClassVar, Optional 6 | 7 | from pydantic import BaseModel, Field 8 | 9 | from mmio.core.logger import LoggerManager 10 | from mmio.infrastructure.file_handling.base_output_manager import OutputManager 11 | 12 | logger = LoggerManager.get_logger(__name__) 13 | 14 | 15 | class VerilogFileManager(BaseModel): 16 | """File Manager to handle Verilog file operations. 17 | 18 | Specialized manager for handling Verilog file output operations. 19 | Uses composition with OutputManager for file operations. 20 | 21 | Attributes: 22 | output_manager: Output manager instance for file operations 23 | 24 | """ 25 | 26 | _instance: ClassVar[Optional["VerilogFileManager"]] = None 27 | output_manager: OutputManager = Field(default_factory=OutputManager.get_instance) 28 | 29 | @classmethod 30 | def get_instance(cls) -> "VerilogFileManager": 31 | """Get the singleton instance of VerilogFileManager.""" 32 | if cls._instance is None: 33 | cls._instance = cls() 34 | return cls._instance 35 | 36 | def write_file(self, content: str) -> None: 37 | """Write generated Verilog content to an output file.""" 38 | if not self.output_manager.file_manager.file_name: 39 | self.generate_output_filename() 40 | self.output_manager.write_file(content) 41 | 42 | def generate_output_filename(self) -> str: 43 | """Generate output filename with timestamp.""" 44 | timestamp = datetime.now().strftime("%Y%m%d%H%M%S") 45 | filename = f"generated_verilog_{timestamp}.v" 46 | self.output_manager.file_manager.file_name = filename 47 | logger.info(f"Generated output filename: {filename}") 48 | return filename 49 | 50 | 51 | verilog_filemanager: VerilogFileManager = VerilogFileManager.get_instance() 52 | -------------------------------------------------------------------------------- /mmio/infrastructure/file_handling/base_file_manager.py: -------------------------------------------------------------------------------- 1 | """Base file manager with common functionality.""" 2 | 3 | from pathlib import Path 4 | from typing import Optional 5 | 6 | from pydantic import BaseModel, ConfigDict, Field, field_validator 7 | 8 | from mmio.core.logger import LoggerManager 9 | 10 | logger = LoggerManager.get_logger(__name__) 11 | 12 | 13 | class BaseFileManager(BaseModel): 14 | """Base class for all file managers with common Pydantic configuration. 15 | 16 | This class provides common functionality for file operations and path management. 17 | It uses Pydantic for validation and type checking. 18 | 19 | Attributes: 20 | path: Base path for file operations 21 | file_name: Name of the file to process 22 | 23 | """ 24 | 25 | model_config = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True, extra="forbid") 26 | 27 | path: Path | None = Field(default=None) 28 | file_name: str | None = Field(default=None) 29 | 30 | def validate_path(self) -> None: 31 | """Validate the base path.""" 32 | if self.path is None: 33 | raise ValueError("Path is not set") 34 | if not self.path.exists(): 35 | self.path.mkdir(parents=True, exist_ok=True) 36 | if not self.path.is_dir(): 37 | raise ValueError(f"Path is not a directory: {self.path}") 38 | 39 | def validate_file(self, file_path: Path) -> None: 40 | """Validate a file path.""" 41 | if not file_path.exists(): 42 | raise ValueError(f"File does not exist: {file_path}") 43 | if not file_path.is_file(): 44 | raise ValueError(f"Path exists but is not a file: {file_path}") 45 | 46 | def list_files(self, pattern: str = "*") -> list[Path]: 47 | """List files in the directory with optional pattern matching.""" 48 | self.validate_path() 49 | if self.path is None: 50 | return [] 51 | try: 52 | files = list(self.path.glob(pattern)) 53 | logger.info(f"Found {len(files)} files matching pattern '{pattern}' in {self.path}") 54 | return files 55 | except Exception as e: 56 | logger.error(f"Error listing files: {e}") 57 | return [] 58 | -------------------------------------------------------------------------------- /mmio/application/cli/commands/file_select.py: -------------------------------------------------------------------------------- 1 | """File select CLI.""" 2 | 3 | from typing import Any 4 | 5 | from click import Argument, Command, Group, Option 6 | from pydantic import BaseModel, ConfigDict, Field 7 | 8 | from mmio.core.logger import LoggerManager 9 | from mmio.infrastructure.cli.base import CLIBase, CLICommand 10 | 11 | logger = LoggerManager.get_logger(__name__) 12 | 13 | 14 | class BaseFileSelectCLI(BaseModel): 15 | """Base select CLI.""" 16 | 17 | model_config = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True) 18 | 19 | cli_base: CLIBase = Field(default_factory=CLIBase) 20 | cli_commands: CLICommand = Field(default_factory=CLICommand) 21 | 22 | @property 23 | def file_group(self) -> Group: 24 | """Get the file group.""" 25 | return Group(name=self.file_name) 26 | 27 | @property 28 | def file_name(self) -> str: 29 | """Get the file name.""" 30 | return "file" 31 | 32 | def __init__(self) -> None: 33 | """Initialize the file select CLI.""" 34 | super().__init__() 35 | self.cli_commands.name = self.file_name 36 | self.cli_commands.group = self.file_group 37 | self.cli_commands.help = "File select CLI." 38 | self.cli_commands.epilog = "File select CLI." 39 | self.cli_commands.short_help = "File select CLI." 40 | self.cli_commands.options_metavar = "[OPTIONS]" 41 | self.cli_commands.add_help_option = True 42 | self.cli_commands.no_args_is_help = False 43 | self.cli_commands.hidden = False 44 | self.cli_commands.deprecated = False 45 | 46 | def add_command(self, command: Command) -> None: 47 | """Add a command to the CLI.""" 48 | self.cli_base.add_command(command=command) 49 | 50 | def add_argument(self, *args: Any, **kwargs: Any) -> None: 51 | """Add an argument to the CLI.""" 52 | self.cli_commands.params.append(Argument(*args, **kwargs)) 53 | 54 | def add_option(self, *args: Any, **kwargs: Any) -> None: 55 | """Add an option to the CLI.""" 56 | self.cli_commands.params.append(Option(*args, **kwargs)) 57 | 58 | def add_help_option(self) -> None: 59 | """Add a help option to the CLI.""" 60 | self.cli_commands.add_help_option = True 61 | 62 | def add_group(self, group: Group) -> None: 63 | """Add a group to the CLI.""" 64 | self.cli_base.add_group(group=group) 65 | 66 | def prompt_user(self) -> None: 67 | """Prompt the user for input.""" 68 | print("Please select a file from the list ...") 69 | -------------------------------------------------------------------------------- /mmio/application/cli/commands/output_select.py: -------------------------------------------------------------------------------- 1 | """Output select CLI.""" 2 | 3 | from typing import Any 4 | 5 | from click import Argument, Command, Group, Option 6 | from pydantic import BaseModel, ConfigDict, Field 7 | 8 | from mmio.core.logger import LoggerManager 9 | from mmio.infrastructure.cli.base import CLIBase, CLICommand 10 | 11 | logger = LoggerManager.get_logger(__name__) 12 | 13 | 14 | class OutputVerilogCLI(BaseModel): 15 | """Output Verilog CLI.""" 16 | 17 | model_config = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True) 18 | 19 | cli_base: CLIBase = Field(default_factory=CLIBase) 20 | cli_commands: CLICommand = Field(default_factory=CLICommand) 21 | 22 | @property 23 | def output_group(self) -> Group: 24 | """Get the output group.""" 25 | return Group(name=self.output_name) 26 | 27 | @property 28 | def output_name(self) -> str: 29 | """Get the output name.""" 30 | return "output" 31 | 32 | def __init__(self) -> None: 33 | """Initialize the output select CLI.""" 34 | super().__init__() 35 | self.cli_commands.name = self.output_name 36 | self.cli_commands.group = self.output_group 37 | self.cli_commands.help = "Output select CLI." 38 | self.cli_commands.epilog = "Output select CLI." 39 | self.cli_commands.short_help = "Output select CLI." 40 | self.cli_commands.options_metavar = "[OPTIONS]" 41 | self.cli_commands.add_help_option = True 42 | self.cli_commands.no_args_is_help = False 43 | self.cli_commands.hidden = False 44 | self.cli_commands.deprecated = False 45 | 46 | def add_command(self, command: Command) -> None: 47 | """Add a command to the CLI.""" 48 | self.cli_base.add_command(command=command) 49 | 50 | def add_argument(self, *args: Any, **kwargs: Any) -> None: 51 | """Add an argument to the CLI.""" 52 | self.cli_commands.params.append(Argument(*args, **kwargs)) 53 | 54 | def add_option(self, *args: Any, **kwargs: Any) -> None: 55 | """Add an option to the CLI.""" 56 | self.cli_commands.params.append(Option(*args, **kwargs)) 57 | 58 | def add_help_option(self) -> None: 59 | """Add a help option to the CLI.""" 60 | self.cli_commands.add_help_option = True 61 | 62 | def add_group(self, group: Group) -> None: 63 | """Add a group to the CLI.""" 64 | self.cli_base.add_group(group=group) 65 | 66 | def prompt_user(self) -> None: 67 | """Prompt the user for input.""" 68 | print("Please select an output format ...") 69 | -------------------------------------------------------------------------------- /mmio/application/cli/coordinator/cli_coordinator.py: -------------------------------------------------------------------------------- 1 | import click 2 | from pydantic import BaseModel, ConfigDict, Field 3 | 4 | from mmio.application.cli.commands.mmio_select import SelectMMIOFileInputCLI 5 | from mmio.application.cli.commands.output_select import OutputVerilogCLI 6 | from mmio.config.cli_config import CLIOptions 7 | from mmio.core.logger import LoggerManager 8 | 9 | logger = LoggerManager.get_logger(__name__) 10 | 11 | 12 | class CLICoordinator(BaseModel): 13 | """Central coordinator for CLI interactions. 14 | 15 | This class calls individual CLI modules sequentially and aggregates 16 | the responses into a CLIOptions object that can be passed to the orchestrators. 17 | """ 18 | 19 | model_config = ConfigDict(arbitrary_types_allowed=True) 20 | 21 | mmio_select_cli: SelectMMIOFileInputCLI = Field(default_factory=SelectMMIOFileInputCLI) 22 | output_select_cli: OutputVerilogCLI = Field(default_factory=OutputVerilogCLI) 23 | 24 | def collect_options(self) -> CLIOptions: 25 | """Interactively collect CLI options from the user.""" 26 | self.mmio_select_cli.prompt_user() 27 | self.output_select_cli.prompt_user() 28 | 29 | bar_input = click.prompt( 30 | "Enter BAR numbers separated by commas (or type 'all' to process all BARs)", 31 | default="all", 32 | ) 33 | if bar_input.strip().lower() == "all": 34 | bar_selection = None 35 | else: 36 | try: 37 | bar_selection = list({int(b.strip()) for b in bar_input.split(",")}) 38 | except ValueError: 39 | click.echo("Invalid input for BAR numbers. Processing all BARs by default.") 40 | bar_selection = None 41 | 42 | op_filter = click.prompt("Select operation type: R for Read, W for Write, B for Both", default="B").upper() 43 | 44 | include_address_checks = click.confirm("Generate Address Check functions?", default=True) 45 | include_counters = click.confirm("Generate Counter functions?", default=True) 46 | include_logic = click.confirm("Generate Logic functions?", default=True) 47 | include_state_machines = click.confirm("Generate State Machine functions?", default=True) 48 | init_roms = click.confirm("Generate ROM Initialization functions?", default=True) 49 | 50 | return CLIOptions( 51 | bar_selection=bar_selection, 52 | operation_filter=op_filter, 53 | include_address_checks=include_address_checks, 54 | include_counters=include_counters, 55 | include_logic=include_logic, 56 | include_state_machines=include_state_machines, 57 | init_roms=init_roms, 58 | ) 59 | -------------------------------------------------------------------------------- /mmio/config/verilog_settings.py: -------------------------------------------------------------------------------- 1 | """Configuration for Verilog code generation output.""" 2 | 3 | from pathlib import Path 4 | from typing import Any, ClassVar, Optional 5 | 6 | from pydantic import Field, ValidationInfo, field_validator 7 | from pydantic_settings import BaseSettings, SettingsConfigDict 8 | 9 | from mmio.config.base_config import BaseConfig 10 | from mmio.core.logger import LoggerManager 11 | 12 | logger = LoggerManager.get_logger(__name__) 13 | 14 | 15 | class VerilogSettings(BaseSettings): 16 | """Configuration for Verilog code generation output.""" 17 | 18 | _instance: ClassVar[Optional["VerilogSettings"]] = None 19 | 20 | # From BaseConfig 21 | base_settings: BaseConfig = Field(default_factory=BaseConfig.get_instance) 22 | 23 | module_header: str | None = "cool_bar_controller" 24 | file_output_path: Path | None = Field( 25 | default_factory=lambda: BaseConfig.get_instance().workdir / "output" / "verilog" 26 | ) 27 | file_output_name: str | None = Field(default=None) 28 | 29 | model_config = SettingsConfigDict( 30 | env_file=".env", 31 | env_file_encoding="utf-8", 32 | case_sensitive=False, 33 | env_ignore_empty=True, 34 | extra="allow", 35 | env_prefix="VERILOG_", 36 | ) 37 | 38 | @classmethod 39 | def get_instance(cls) -> "VerilogSettings": 40 | """Get the singleton instance of VerilogSettings.""" 41 | if cls._instance is None: 42 | cls._instance = cls() 43 | return cls._instance 44 | 45 | @field_validator("file_output_path", mode="before") 46 | @classmethod 47 | def validate_directory_exists(cls, value: Path | str, info: ValidationInfo) -> Path: 48 | """Ensure the Verilog output directory exists if a path is provided.""" 49 | if not isinstance(value, Path): 50 | value = Path(value) 51 | if value.exists(): 52 | return value 53 | value.mkdir(parents=True, exist_ok=True) 54 | logger.error(f"Verilog output directory did not exist, creating: {value}") 55 | return value 56 | 57 | @field_validator("file_output_name", mode="after") 58 | @classmethod 59 | def validate_file_exists(cls, value: str, info: ValidationInfo) -> str: 60 | """Ensure the Verilog output file exists.""" 61 | directory: Any | None = info.data.get("file_output_path") 62 | if directory is None: 63 | raise ValueError("Verilog output directory path is not set.") 64 | if value is not None: 65 | path: Path = directory / value 66 | if not path.is_file(): 67 | raise ValueError(f"Verilog output file does not exist: {path}") 68 | return value 69 | 70 | 71 | settings: VerilogSettings = VerilogSettings.get_instance() 72 | -------------------------------------------------------------------------------- /mmio/infrastructure/file_handling/base_output_manager.py: -------------------------------------------------------------------------------- 1 | """Base Output File Manager for writing operations.""" 2 | 3 | from pathlib import Path 4 | from typing import ClassVar, Optional, Self 5 | 6 | from pydantic import BaseModel, Field, model_validator 7 | 8 | from mmio.config.verilog_settings import VerilogSettings 9 | from mmio.core.logger import LoggerManager 10 | from mmio.infrastructure.file_handling.base_file_manager import BaseFileManager 11 | 12 | logger = LoggerManager.get_logger(__name__) 13 | 14 | 15 | class OutputManager(BaseModel): 16 | """Output file manager for writing operations. 17 | 18 | Provides functionality for writing and managing output files. 19 | Uses Pydantic for validation and type checking. 20 | 21 | Attributes: 22 | settings: Verilog settings for output operations 23 | file_manager: Base file manager for file operations 24 | folder_name: Name of the output folder 25 | 26 | """ 27 | 28 | model_config = BaseFileManager.model_config 29 | 30 | _instance: ClassVar[Optional["OutputManager"]] = None 31 | 32 | settings: VerilogSettings = Field(default_factory=VerilogSettings.get_instance) 33 | file_manager: BaseFileManager = Field(default_factory=BaseFileManager) 34 | folder_name: str | None = Field(default=None) 35 | 36 | @classmethod 37 | def get_instance(cls) -> "OutputManager": 38 | """Get the singleton instance of the output manager.""" 39 | if cls._instance is None: 40 | cls._instance = cls() 41 | return cls._instance 42 | 43 | @model_validator(mode="after") 44 | def set_default_paths(self) -> Self: 45 | """Set default paths from settings if not provided.""" 46 | if self.file_manager.path is None: 47 | object.__setattr__(self.file_manager, "path", self.settings.file_output_path) 48 | return self 49 | 50 | def get_full_output_path(self) -> Path | None: 51 | """Get the full output path combining directory and filename.""" 52 | if self.file_manager.path and self.file_manager.file_name: 53 | return self.file_manager.path / self.file_manager.file_name 54 | return None 55 | 56 | def write_file(self, content: str) -> None: 57 | """Write content to output file.""" 58 | self.file_manager.validate_path() 59 | if self.file_manager.path is None: 60 | raise ValueError("Output path is not set") 61 | if self.file_manager.file_name is None: 62 | raise ValueError("Output file name is not set") 63 | 64 | file_path = self.get_full_output_path() 65 | if file_path is None: 66 | raise ValueError("Could not construct output file path") 67 | 68 | logger.info(f"Writing file: {file_path}") 69 | file_path.write_text(content) 70 | -------------------------------------------------------------------------------- /mmio/config/mmio_settings.py: -------------------------------------------------------------------------------- 1 | """Configuration from .env file and paths.""" 2 | 3 | import os 4 | from pathlib import Path 5 | from typing import ClassVar, Optional 6 | 7 | from pydantic import Field, ValidationInfo, field_validator 8 | from pydantic_settings import BaseSettings, SettingsConfigDict 9 | 10 | from mmio.config.base_config import BaseConfig 11 | from mmio.core.logger import LoggerManager 12 | 13 | logger = LoggerManager.get_logger(__name__) 14 | 15 | 16 | class MMIOSettings(BaseSettings): 17 | """Configuration for MMIO log file input handling.""" 18 | 19 | _instance: ClassVar[Optional["MMIOSettings"]] = None 20 | 21 | # From BaseConfig 22 | base_settings: BaseConfig = Field(default_factory=BaseConfig.get_instance) 23 | workdir: Path = Field(default_factory=lambda: BaseConfig.get_instance().workdir) 24 | file_input_path: Path | None = Field(default_factory=lambda: BaseConfig.get_instance().workdir / "input" / "mmio") 25 | file_input_name: str | None = Field(default=None) 26 | 27 | model_config = SettingsConfigDict( 28 | env_file=".env", 29 | env_file_encoding="utf-8", 30 | case_sensitive=False, 31 | env_ignore_empty=True, 32 | extra="allow", 33 | env_prefix="MMIO_", 34 | ) 35 | 36 | @classmethod 37 | def get_instance(cls) -> "MMIOSettings": 38 | """Get the singleton instance of MMIOSettings.""" 39 | if cls._instance is None: 40 | cls._instance = cls() 41 | return cls._instance 42 | 43 | @field_validator("workdir", mode="before") 44 | @classmethod 45 | def ensure_workdir(cls, value: Path | str) -> Path: 46 | """Ensure workdir is a Path and exists.""" 47 | if not isinstance(value, Path): 48 | value = Path(str(value)) 49 | if not value.exists(): 50 | value.mkdir(parents=True, exist_ok=True) 51 | return value 52 | 53 | @field_validator("file_input_path", mode="after") 54 | @classmethod 55 | def resolve_input_path(cls, value: Path, info: ValidationInfo) -> Path: 56 | """Resolve the input path relative to workdir if it's not absolute.""" 57 | if not value.is_absolute(): 58 | workdir = info.data.get("workdir") 59 | if workdir is None: 60 | workdir = BaseConfig.get_instance().workdir 61 | value = workdir / value 62 | if not value.exists(): 63 | value.mkdir(parents=True, exist_ok=True) 64 | logger.info(f"Created input directory: {value}") 65 | return value 66 | 67 | @field_validator("file_input_name", mode="before") 68 | @classmethod 69 | def validate_file_exists(cls, value: str, info: ValidationInfo) -> str: 70 | """Ensure the input log file exists.""" 71 | directory: Path | None = info.data.get("file_input_path") 72 | if directory is None: 73 | raise ValueError("Input directory path is not set.") 74 | if value is not None: 75 | path: Path = directory / value 76 | if not path.is_file(): 77 | raise ValueError(f"Input log file does not exist: {path}") 78 | return value 79 | 80 | 81 | settings: MMIOSettings = MMIOSettings.get_instance() 82 | -------------------------------------------------------------------------------- /mmio/infrastructure/file_handling/base_input_manager.py: -------------------------------------------------------------------------------- 1 | """Base Input File Manager for reading operations.""" 2 | 3 | from pathlib import Path 4 | from typing import ClassVar, Optional, Self 5 | 6 | from pydantic import BaseModel, Field, model_validator 7 | 8 | from mmio.config.mmio_settings import MMIOSettings 9 | from mmio.core.logger import LoggerManager 10 | from mmio.infrastructure.file_handling.base_file_manager import BaseFileManager 11 | 12 | logger = LoggerManager.get_logger(__name__) 13 | 14 | 15 | class InputManager(BaseModel): 16 | """Input file manager for reading operations. 17 | 18 | Provides functionality for reading and managing input files. 19 | Uses Pydantic for validation and type checking. 20 | 21 | Attributes: 22 | mmio_settings: MMIO settings for input operations 23 | file_manager: Base file manager for file operations 24 | 25 | """ 26 | 27 | model_config = BaseFileManager.model_config 28 | 29 | _instance: ClassVar[Optional["InputManager"]] = None 30 | 31 | mmio_settings: MMIOSettings = Field(default_factory=MMIOSettings.get_instance) 32 | file_manager: BaseFileManager = Field(default_factory=BaseFileManager) 33 | 34 | @classmethod 35 | def get_instance(cls) -> "InputManager": 36 | """Get the singleton instance of the input manager.""" 37 | if cls._instance is None: 38 | cls._instance = cls() 39 | return cls._instance 40 | 41 | @model_validator(mode="after") 42 | def set_default_paths(self) -> Self: 43 | """Set default paths from settings if not provided.""" 44 | if self.file_manager.path is None: 45 | object.__setattr__(self.file_manager, "path", self.mmio_settings.file_input_path) 46 | if self.file_manager.file_name is None: 47 | object.__setattr__(self.file_manager, "file_name", self.mmio_settings.file_input_name) 48 | return self 49 | 50 | def get_full_input_path(self) -> Path | None: 51 | """Get the full input path combining directory and filename.""" 52 | if self.file_manager.path and self.file_manager.file_name: 53 | logger.info(f"Constructing path from: {self.file_manager.path} and {self.file_manager.file_name}") 54 | return self.file_manager.path / self.file_manager.file_name 55 | logger.error(f"Cannot construct path - path: {self.file_manager.path}, filename: {self.file_manager.file_name}") 56 | return None 57 | 58 | def read_file(self) -> Path: 59 | """Read configured input file with validation. 60 | 61 | Returns: 62 | Path: Validated path to the input file 63 | 64 | Raises: 65 | ValueError: If input path or file name is not set 66 | FileNotFoundError: If file doesn't exist or isn't accessible 67 | 68 | """ 69 | self.file_manager.validate_path() 70 | if self.file_manager.path is None: 71 | raise ValueError("Input path is not set") 72 | if self.file_manager.file_name is None: 73 | raise ValueError("Input file name is not set") 74 | 75 | file_path = self.get_full_input_path() 76 | if file_path is None: 77 | raise ValueError("Could not construct input file path") 78 | 79 | self.file_manager.validate_file(file_path) 80 | logger.info(f"Validated input file: {file_path}") 81 | return file_path 82 | 83 | def list_files(self, pattern: str = "*") -> list[Path]: 84 | """List files in the directory with optional pattern matching.""" 85 | return self.file_manager.list_files(pattern) 86 | -------------------------------------------------------------------------------- /mmio/core/mmio_parser.py: -------------------------------------------------------------------------------- 1 | """MMIO Parser module.""" 2 | 3 | from collections import OrderedDict 4 | from typing import Any 5 | 6 | from pydantic import BaseModel, ConfigDict, Field 7 | 8 | from mmio.core.exceptions import ( 9 | FileAccessError, 10 | MMIOParserError, 11 | ) 12 | from mmio.core.logger import LoggerManager 13 | from mmio.core.parse_logic import MMIOParseLogic 14 | 15 | logger = LoggerManager.get_logger(__name__) 16 | 17 | 18 | class MMIOParser(BaseModel): 19 | """Parser for MMIO logs. 20 | 21 | This class handles parsing of MMIO log content, converting raw log lines into structured data. 22 | Each line represents either a read or write operation with associated metadata. 23 | 24 | Attributes: 25 | parse_logic: Logic for parsing MMIO log lines 26 | 27 | """ 28 | 29 | model_config = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True) 30 | parse_logic: MMIOParseLogic = Field(default_factory=MMIOParseLogic) 31 | 32 | def parse_content(self, content: str) -> list[dict[str, Any]]: 33 | """Parse MMIO content and return a list of parsed data as dictionaries. 34 | 35 | Args: 36 | content: MMIO log content to parse 37 | 38 | Returns: 39 | List of dictionaries containing parsed MMIO operations, including: 40 | - All original fields from parse_line 41 | - read_value/write_value: Operation-specific values 42 | - read_address/write_address: Operation-specific addresses (shifted) 43 | 44 | Raises: 45 | FileAccessError: If content is empty 46 | MMIOParserError: For unexpected errors 47 | 48 | """ 49 | try: 50 | if not content.strip(): 51 | logger.info("Skipping empty content") 52 | return [] 53 | 54 | lines: list[str] = content.splitlines() 55 | parsed_data: list[dict[str, Any]] = [] 56 | 57 | for line_number, line in enumerate(iterable=lines, start=1): 58 | if not (line.startswith("R") or line.startswith("W")): 59 | logger.info(f"Skipped non-R/W line {line_number}: {line[:40]}...") 60 | continue 61 | 62 | try: 63 | parsed_line_dict: OrderedDict[str, Any] = ( 64 | self.parse_logic.parse_line(line) 65 | ) 66 | logger.info( 67 | f"Parsed line: {parsed_line_dict['operation']}, {parsed_line_dict['address']}, {parsed_line_dict['value']}" 68 | ) 69 | value = parsed_line_dict["value"] 70 | num = int(value, 16) 71 | value = f"{num:08X}" 72 | logger.info(f"Parsed value: {value}") 73 | 74 | parsed_data.append( 75 | { 76 | "operation": parsed_line_dict["operation"], 77 | "counter": parsed_line_dict["counter"], 78 | "timestamp": float(parsed_line_dict["timestamp"]), 79 | "bar": int(parsed_line_dict["bar_number"]), 80 | "address": parsed_line_dict["address"], 81 | "value": value, 82 | } 83 | ) 84 | logger.info(f"Parsed data: {parsed_data[-1]}") 85 | except (ValueError, KeyError) as e: 86 | logger.info(f"Skipped invalid line {line_number}: {str(e)}") 87 | continue 88 | 89 | return parsed_data 90 | except FileAccessError: 91 | raise 92 | except Exception as e: 93 | raise MMIOParserError(f"Unexpected error: {str(e)}") 94 | -------------------------------------------------------------------------------- /mmio/domain/services/orchestrators/input_orchestrator.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Any 3 | 4 | from pydantic import BaseModel, Field 5 | 6 | from mmio.core.logger import LoggerManager 7 | from mmio.core.mmio_parser import MMIOParser 8 | from mmio.domain.models.verilog_data import VerilogData 9 | from mmio.infrastructure.file_handling.mmio_filemanager import MMIOFileManager 10 | 11 | logger = LoggerManager.get_logger(__name__) 12 | 13 | 14 | class InputOrchestrator(BaseModel): 15 | """Handle the input operations. 16 | 17 | - Reading file(s) using MMIOFileManager. 18 | - Parsing the MMIO log content via MMIOParser. 19 | - Converting the parsed dictionaries into VerilogData instances. 20 | 21 | Returns a processed data structure (e.g., a dictionary of VerilogData) that will be used 22 | by the Verilog builder orchestrator. 23 | """ 24 | 25 | mmio_file_manager: MMIOFileManager = Field(default_factory=MMIOFileManager.get_instance) 26 | parser: MMIOParser = Field(default_factory=MMIOParser) 27 | 28 | def execute(self) -> dict[int, list[VerilogData]]: 29 | """Execute the input processing. 30 | 31 | Process: 32 | 1. Read the input file path using MMIOFileManager. 33 | 2. Read file content. 34 | 3. Parse file content using MMIOParser to get a list of dictionary data. 35 | 4. Transform each parsed dictionary into VerilogData using from_dict. 36 | 5. Aggregate the data into a structured format (e.g., keyed by BAR) and return. 37 | 38 | Returns: 39 | dict: A dictionary containing processed BAR data. 40 | 41 | """ 42 | logger.info("Starting input processing") 43 | 44 | logger.info(f"Reading file from path: {self.mmio_file_manager.input_manager.file_manager.path}") 45 | if self.mmio_file_manager.input_manager.file_manager.file_name is not None: 46 | logger.info(f"Input file name: {self.mmio_file_manager.input_manager.file_manager.file_name}") 47 | else: 48 | logger.info("No input file name provided") 49 | 50 | file_path: Path = self.mmio_file_manager.read_file() 51 | logger.info(f"Successfully read file: {file_path}") 52 | 53 | content: str = file_path.read_text() 54 | 55 | logger.info("Parsing file content") 56 | parsed_results: list[dict[str, Any]] = self.parser.parse_content(content) 57 | 58 | logger.info("Transforming parsed data to VerilogData instances") 59 | processed_data: dict[int, list[VerilogData]] = {} 60 | success_count = 0 61 | error_count = 0 62 | 63 | for entry in parsed_results: 64 | try: 65 | verilog_data: VerilogData = VerilogData.from_dict(entry) 66 | logger.info(f"Created VerilogData instance: {verilog_data}") 67 | 68 | bar_number: int = verilog_data.bar if verilog_data.bar is not None else 0 69 | if bar_number not in processed_data: 70 | processed_data[bar_number] = [] 71 | logger.info(f"Created new list for BAR {bar_number}") 72 | processed_data[bar_number].append(verilog_data) 73 | success_count += 1 74 | 75 | if success_count % 100 == 0: 76 | logger.info(f"Processed {success_count} entries successfully") 77 | except Exception as e: 78 | error_count += 1 79 | logger.error(f"Error processing entry: {e}") 80 | logger.info(f"Problematic entry: {entry}") 81 | 82 | if error_count > 0: 83 | logger.warning(f"Failed to process {error_count} entries") 84 | 85 | logger.info(f"Final processed data contains {len(processed_data)} BARs") 86 | for bar_num, entries in processed_data.items(): 87 | logger.info(f"BAR {bar_num}: {len(entries)} entries") 88 | 89 | return processed_data 90 | -------------------------------------------------------------------------------- /mmio/infrastructure/cli/base.py: -------------------------------------------------------------------------------- 1 | """Base class for CLI.""" 2 | 3 | from typing import Any 4 | 5 | import click 6 | from click import Argument, Command, Group, Option 7 | from pydantic import BaseModel, ConfigDict 8 | 9 | from mmio.core.logger import LoggerManager 10 | 11 | logger = LoggerManager.get_logger(__name__) 12 | 13 | 14 | class CLIBase(BaseModel): 15 | """Base class for CLI.""" 16 | 17 | model_config = ConfigDict(arbitrary_types_allowed=True) 18 | name: str | None = None 19 | group: Group | Command | None = None 20 | 21 | click_group: Group = Group(name=None) 22 | click_command: Command = Command(name=None) 23 | click_argument: Argument = Argument( 24 | param_decls=["argument_name"], 25 | required=False, 26 | ) 27 | click_option: Option = Option( 28 | param_decls=["--option_name"], 29 | required=False, 30 | ) 31 | 32 | def setup_commands(self) -> None: 33 | """Set up all commands for the CLI.""" 34 | if self.name is not None: 35 | self.click_group.add_command(cmd=Command(name=self.name)) 36 | if self.group is not None: 37 | self.click_group.add_command(cmd=self.group) 38 | 39 | def add_command(self, command: Command) -> None: 40 | """Add a command to the CLI.""" 41 | self.click_group.add_command(cmd=command) 42 | 43 | def add_group(self, group: Group) -> None: 44 | """Add a group to the CLI.""" 45 | self.click_group.add_command(cmd=group) 46 | 47 | def add_argument(self, *args: Any, **kwargs: Any) -> None: 48 | """Add an argument to the CLI.""" 49 | self.click_command.params.append(Argument(*args, **kwargs)) 50 | 51 | def add_option(self, *args: Any, **kwargs: Any) -> None: 52 | """Add an option to the CLI.""" 53 | self.click_command.params.append(Option(*args, **kwargs)) 54 | 55 | 56 | class CLICommand(click.Command): 57 | """Command class.""" 58 | 59 | def __init__(self, *args: Any, **kwargs: Any) -> None: 60 | """Initialize the command with a name.""" 61 | self.group = kwargs.pop("group", None) 62 | params = kwargs.pop("params", []) 63 | self.help = kwargs.pop("help", "") 64 | self.epilog = kwargs.pop("epilog", "") 65 | self.short_help = kwargs.pop("short_help", None) 66 | self.options_metavar = kwargs.pop("options_metavar", "[OPTIONS]") 67 | self.add_help_option = kwargs.pop("add_help_option", True) 68 | self.no_args_is_help = kwargs.pop("no_args_is_help", False) 69 | self.hidden = kwargs.pop("hidden", False) 70 | self.deprecated = kwargs.pop("deprecated", False) 71 | 72 | if "name" not in kwargs: 73 | kwargs["name"] = "default_name" 74 | 75 | kwargs["params"] = [] 76 | super().__init__(*args, **kwargs) 77 | 78 | for param in params: 79 | self.params.append(param) 80 | 81 | def add_parameter(self, param: click.Parameter) -> None: 82 | """Add a parameter to the command while checking for name conflicts. 83 | 84 | Args: 85 | param: The Click parameter to add 86 | 87 | Raises: 88 | ValueError: If a parameter with the same name or short form already exists 89 | 90 | """ 91 | existing_names = {p.name for p in self.params} 92 | if param.name in existing_names: 93 | raise ValueError(f"Parameter with name '{param.name}' already exists") 94 | 95 | if isinstance(param, click.Option): 96 | new_shorts = {opt[1:] for opt in param.opts if len(opt) == 2 and opt.startswith("-")} 97 | existing_shorts = { 98 | opt[1:] 99 | for p in self.params 100 | if isinstance(p, click.Option) 101 | for opt in p.opts 102 | if len(opt) == 2 and opt.startswith("-") 103 | } 104 | 105 | conflicts = new_shorts & existing_shorts 106 | if conflicts: 107 | raise ValueError(f"Short form(s) '-{', -'.join(conflicts)}' already in use by another option") 108 | 109 | self.params.append(param) 110 | 111 | def to_info_dict(self, ctx: click.Context) -> dict[str, Any]: 112 | """Convert the command to a dictionary.""" 113 | info_dict: dict[str, Any] = super().to_info_dict(ctx) 114 | info_dict.update( 115 | params=[param.to_info_dict() for param in self.params], 116 | ) 117 | return info_dict 118 | 119 | 120 | model_config = ConfigDict(arbitrary_types_allowed=True) 121 | -------------------------------------------------------------------------------- /mmio/application/verilog/generators/address_check.py: -------------------------------------------------------------------------------- 1 | """Generator module for Verilog ROM address checking functions.""" 2 | 3 | from io import StringIO 4 | 5 | from pydantic import Field 6 | 7 | from mmio.application.verilog.verilog_models import AddressCheckEntry, VerilogGenerator 8 | from mmio.core.logger import LoggerManager 9 | from mmio.domain.models.verilog_data import VerilogData 10 | 11 | logger = LoggerManager.get_logger(__name__) 12 | 13 | 14 | class AddressCheckGenerator(VerilogGenerator): 15 | """Generate ROM address checking functions and save them in an AddressCheckEntry.""" 16 | 17 | verilog_data: VerilogData = Field(default_factory=VerilogData) 18 | 19 | verilog_address_check: AddressCheckEntry = Field( 20 | default_factory=lambda: AddressCheckEntry( 21 | bar_number=0, 22 | verilog_read_addr_check=StringIO(), 23 | verilog_write_addr_check=StringIO(), 24 | ) 25 | ) 26 | 27 | def generate_read_address_check(self, bar_number: int) -> None: 28 | """Generate read address check function for specific BAR.""" 29 | read_addrs = [ 30 | data.address 31 | for data in VerilogData.get_all_instances() 32 | if data.operation == "R" and data.address is not None and data.bar == bar_number 33 | ] 34 | sorted_read = self.get_unique_sorted_addresses(read_addrs) 35 | 36 | sio_read = StringIO() 37 | if sorted_read: 38 | sio_read.write(" function read_addr_check;\n") 39 | sio_read.write(" input [19:0] addr;\n") 40 | sio_read.write(" begin\n") 41 | sio_read.write(" case (addr)\n") 42 | for i in range(0, len(sorted_read), 8): 43 | addr_group = sorted_read[i : i + 8] 44 | sio_read.write(" ") 45 | sio_read.write(", ".join(f"20'h{addr.zfill(4)}" for addr in addr_group)) 46 | if i + 8 < len(sorted_read): 47 | sio_read.write(",\n") 48 | else: 49 | sio_read.write(":\n") 50 | sio_read.write(" read_addr_check = 1'b1;\n") 51 | sio_read.write(" default: read_addr_check = 1'b0;\n") 52 | sio_read.write(" endcase\n") 53 | sio_read.write(" end\n") 54 | sio_read.write(" endfunction\n\n") 55 | 56 | self.verilog_address_check.verilog_read_addr_check = StringIO(sio_read.getvalue()) 57 | self.verilog_address_check.verilog_read_addr_check.seek(0) 58 | 59 | def generate_write_address_check(self, bar_number: int) -> None: 60 | """Generate write address check function for specific BAR.""" 61 | write_addrs = [ 62 | data.address 63 | for data in VerilogData.get_all_instances() 64 | if data.operation == "W" and data.address is not None and data.bar == bar_number 65 | ] 66 | sorted_write = self.get_unique_sorted_addresses(write_addrs) 67 | 68 | sio_write = StringIO() 69 | if sorted_write: 70 | sio_write.write(" function write_addr_check;\n") 71 | sio_write.write(" input [19:0] addr;\n") 72 | sio_write.write(" begin\n") 73 | sio_write.write(" case (addr)\n") 74 | for i in range(0, len(sorted_write), 8): 75 | addr_group = sorted_write[i : i + 8] 76 | sio_write.write(" ") 77 | sio_write.write(", ".join(f"20'h{addr.zfill(4)}" for addr in addr_group)) 78 | if i + 8 < len(sorted_write): 79 | sio_write.write(",\n") 80 | else: 81 | sio_write.write(":\n") 82 | sio_write.write(" write_addr_check = 1'b1;\n") 83 | sio_write.write(" default: write_addr_check = 1'b0;\n") 84 | sio_write.write(" endcase\n") 85 | sio_write.write(" end\n") 86 | sio_write.write(" endfunction\n\n") 87 | 88 | self.verilog_address_check.verilog_write_addr_check = StringIO(sio_write.getvalue()) 89 | self.verilog_address_check.verilog_write_addr_check.seek(0) 90 | 91 | def generate_address_check_entry(self, bar_number: int) -> AddressCheckEntry: 92 | """Generate a consolidated AddressCheckEntry with one read and one write function per BAR.""" 93 | self.verilog_address_check.bar_number = bar_number 94 | self.generate_read_address_check(bar_number) 95 | self.generate_write_address_check(bar_number) 96 | 97 | return self.verilog_address_check 98 | -------------------------------------------------------------------------------- /mmio/core/logger.py: -------------------------------------------------------------------------------- 1 | """Centralized logging configuration for the MMIO application.""" 2 | 3 | import logging 4 | import sys 5 | from pathlib import Path 6 | 7 | from pydantic import BaseModel, Field 8 | 9 | 10 | class LogConfig(BaseModel): 11 | """Logging configuration settings. 12 | 13 | Attributes: 14 | level: The logging level to use 15 | format: The log message format 16 | date_format: The format for timestamps 17 | file_path: Optional path to log file 18 | 19 | """ 20 | 21 | level: int = Field(default=logging.WARNING) 22 | format: str = Field(default="%(asctime)s - %(name)s - %(levelname)s - %(message)s") 23 | date_format: str = Field(default="%Y-%m-%d %H:%M:%S") 24 | file_path: Path | None = None 25 | 26 | 27 | class LoggerManager: 28 | """Centralized logger management. 29 | 30 | This class handles the configuration and creation of loggers 31 | throughout the application, ensuring consistent logging behavior. 32 | """ 33 | 34 | _config: LogConfig = LogConfig() 35 | _initialized: bool = False 36 | _loggers: dict[str, logging.Logger] = {} 37 | 38 | @classmethod 39 | def initialize( 40 | cls, 41 | level: int = logging.WARNING, 42 | file_path: Path | None = None, 43 | format_str: str | None = None, 44 | date_format: str | None = None, 45 | ) -> None: 46 | """Initialize the logging system. 47 | 48 | Args: 49 | level: The logging level to use 50 | file_path: Optional path to log file 51 | format_str: Optional custom format string 52 | date_format: Optional custom date format 53 | 54 | """ 55 | if cls._initialized: 56 | return 57 | 58 | cls._config.level = level 59 | if file_path: 60 | cls._config.file_path = file_path 61 | if format_str: 62 | cls._config.format = format_str 63 | if date_format: 64 | cls._config.date_format = date_format 65 | 66 | formatter = logging.Formatter(cls._config.format, cls._config.date_format) 67 | 68 | root_logger = logging.getLogger() 69 | root_logger.setLevel(cls._config.level) 70 | 71 | console_handler = logging.StreamHandler(sys.stdout) 72 | console_handler.setFormatter(formatter) 73 | root_logger.addHandler(console_handler) 74 | 75 | if cls._config.file_path: 76 | try: 77 | file_handler = logging.FileHandler(cls._config.file_path) 78 | file_handler.setFormatter(formatter) 79 | root_logger.addHandler(file_handler) 80 | except Exception as e: 81 | cls.get_logger("LoggerManager").error(f"Failed to create file handler: {e}") 82 | console_handler.emit( 83 | logging.LogRecord( 84 | "LoggerManager", 85 | logging.WARNING, 86 | "", 87 | 0, 88 | f"Failed to create file handler: {e}", 89 | (), 90 | None, 91 | ) 92 | ) 93 | 94 | cls._initialized = True 95 | 96 | @classmethod 97 | def get_logger(cls, name: str) -> logging.Logger: 98 | """Get a logger with the specified name. 99 | 100 | Args: 101 | name: The name for the logger (typically __name__) 102 | 103 | Returns: 104 | A configured logger instance 105 | 106 | """ 107 | if not cls._initialized: 108 | cls.initialize() 109 | 110 | if name not in cls._loggers: 111 | logger = logging.getLogger(name) 112 | cls._loggers[name] = logger 113 | 114 | return cls._loggers[name] 115 | 116 | @classmethod 117 | def set_level(cls, level: int) -> None: 118 | """Set the logging level for all loggers. 119 | 120 | Args: 121 | level: The new logging level 122 | 123 | """ 124 | cls._config.level = level 125 | for logger in cls._loggers.values(): 126 | logger.setLevel(level) 127 | 128 | @classmethod 129 | def add_file_handler(cls, file_path: Path) -> None: 130 | """Add a file handler to all loggers. 131 | 132 | Args: 133 | file_path: Path to the log file 134 | 135 | """ 136 | try: 137 | formatter = logging.Formatter(cls._config.format, cls._config.date_format) 138 | file_handler = logging.FileHandler(file_path) 139 | file_handler.setFormatter(formatter) 140 | 141 | for logger in cls._loggers.values(): 142 | logger.addHandler(file_handler) 143 | 144 | cls._config.file_path = file_path 145 | except Exception as e: 146 | cls.get_logger("LoggerManager").error(f"Failed to add file handler: {e}") 147 | -------------------------------------------------------------------------------- /mmio/config/log_settings.py: -------------------------------------------------------------------------------- 1 | """Logging settings configuration.""" 2 | 3 | import json 4 | import logging 5 | from pathlib import Path 6 | from typing import Any, ClassVar, Optional, Self 7 | 8 | from pydantic import BaseModel, Field, model_validator 9 | from pydantic_settings import SettingsConfigDict 10 | 11 | from mmio.config.base_config import BaseConfig 12 | 13 | LOG_LEVELS: dict[str, int] = { 14 | "DEBUG": logging.DEBUG, 15 | "INFO": logging.INFO, 16 | "WARNING": logging.WARNING, 17 | "ERROR": logging.ERROR, 18 | "CRITICAL": logging.CRITICAL, 19 | } 20 | 21 | 22 | class LogSettings(BaseModel): 23 | """Logging settings configuration. 24 | 25 | This class manages logging settings and their persistence between sessions. 26 | Settings are stored in a temporary file and loaded on startup. 27 | 28 | Attributes: 29 | level: The logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL) 30 | file_enabled: Whether to enable file logging 31 | log_file: Optional path to the log file 32 | format: Log message format string 33 | date_format: Date format for log messages 34 | 35 | """ 36 | 37 | _instance: ClassVar[Optional["LogSettings"]] = None 38 | 39 | # From BaseConfig 40 | base_settings: BaseConfig = Field(default_factory=BaseConfig.get_instance) 41 | workdir: Path = Field(default_factory=lambda: BaseConfig.get_instance().workdir) 42 | format: str = Field(default_factory=lambda: BaseConfig.get_instance().format) 43 | date_format: str = Field(default_factory=lambda: BaseConfig.get_instance().date_format) 44 | 45 | level: str = Field(default="WARNING") 46 | file_enabled: bool = Field(default=True) 47 | log_file: Path | None = Field(default_factory=lambda: BaseConfig.get_instance().workdir / "mmio.log") 48 | settings_file: Path = Field(default_factory=lambda: BaseConfig.get_instance().workdir / "mmio_log_settings.json") 49 | 50 | model_config = SettingsConfigDict( 51 | env_file=".env", 52 | env_file_encoding="utf-8", 53 | case_sensitive=False, 54 | env_ignore_empty=True, 55 | extra="allow", 56 | env_prefix="LOG_", 57 | ) 58 | 59 | @classmethod 60 | def get_instance(cls) -> "LogSettings": 61 | """Get the singleton instance of LogSettings.""" 62 | if cls._instance is None: 63 | cls._instance = cls() 64 | return cls._instance 65 | 66 | @classmethod 67 | def get_workdir(cls) -> Path: 68 | """Get the workspace directory.""" 69 | return BaseConfig.get_instance().workdir 70 | 71 | @model_validator(mode="after") 72 | def validate_settings(self) -> Self: 73 | """Validate the settings after model creation.""" 74 | if self.level not in LOG_LEVELS: 75 | raise ValueError(f"Invalid log level. Must be one of: {', '.join(LOG_LEVELS.keys())}") 76 | return self 77 | 78 | @property 79 | def level_value(self) -> int: 80 | """Get the numeric value for the current log level.""" 81 | return LOG_LEVELS[self.level] 82 | 83 | def save(self, file_path: Path | None = None) -> None: 84 | """Save settings to a file. 85 | 86 | Args: 87 | file_path: Optional custom path to save settings 88 | 89 | """ 90 | save_path: Path = file_path or self.settings_file 91 | 92 | data: dict[str, Any] = self.model_dump() 93 | if data.get("log_file"): 94 | data["log_file"] = str(data["log_file"]) 95 | 96 | try: 97 | save_path.write_text(json.dumps(data, indent=2)) 98 | except Exception as e: 99 | logging.getLogger(__name__).error(f"Failed to save log settings: {e}") 100 | 101 | @classmethod 102 | def load(cls, file_path: Path | None = None) -> "LogSettings": 103 | """Load settings from a file. 104 | 105 | Args: 106 | file_path: Optional custom path to load settings from 107 | 108 | Returns: 109 | LogSettings instance with loaded values or defaults 110 | 111 | """ 112 | load_path: Path = file_path or cls.get_workdir() / "log_settings.json" 113 | 114 | if not load_path.exists(): 115 | return cls() 116 | 117 | try: 118 | data: dict[str, Any] = json.loads(load_path.read_text()) 119 | if data.get("log_file"): 120 | data["log_file"] = Path(data["log_file"]) 121 | return cls(**data) 122 | except Exception as e: 123 | logging.getLogger(__name__).error(f"Failed to load log settings: {e}") 124 | return cls() 125 | 126 | def apply(self) -> None: 127 | """Apply these settings to the LoggerManager.""" 128 | from mmio.core.logger import LoggerManager 129 | 130 | LoggerManager.initialize(level=self.level_value, format_str=self.format, date_format=self.date_format) 131 | 132 | if self.file_enabled and self.log_file: 133 | LoggerManager.add_file_handler(self.log_file) 134 | 135 | 136 | log_settings: LogSettings = LogSettings.get_instance() 137 | -------------------------------------------------------------------------------- /mmio/application/verilog/generators/counter_generator.py: -------------------------------------------------------------------------------- 1 | """Generate Verilog code for counters.""" 2 | 3 | from io import StringIO 4 | 5 | from pydantic import Field 6 | 7 | from mmio.application.verilog.verilog_models import CounterEntry, VerilogGenerator 8 | from mmio.core.logger import LoggerManager 9 | from mmio.domain.models.verilog_data import VerilogData 10 | 11 | logger = LoggerManager.get_logger(__name__) 12 | 13 | 14 | class CounterGenerator(VerilogGenerator): 15 | """Generate Verilog code for counters.""" 16 | 17 | verilog_data: VerilogData = Field(default_factory=VerilogData) 18 | 19 | verilog_counter: CounterEntry = Field( 20 | default_factory=lambda: CounterEntry( 21 | bar_number=0, 22 | verilog_read_counter=StringIO(), 23 | verilog_write_counter=StringIO(), 24 | verilog_reset_read_counter=StringIO(), 25 | verilog_reset_write_counter=StringIO(), 26 | ) 27 | ) 28 | 29 | def generate_read_counters(self, bar_number: int) -> None: 30 | """Generate read counter declarations for a specific BAR.""" 31 | read_addrs = [ 32 | data.address 33 | for data in VerilogData.get_all_instances() 34 | if data.operation == "R" and data.address is not None and data.bar == bar_number 35 | ] 36 | 37 | if not read_addrs: 38 | return 39 | 40 | sio = StringIO() 41 | unique_read_addrs = self.get_unique_sorted_addresses(read_addrs) 42 | for addr in unique_read_addrs: 43 | counter_name = f"R_C_{addr}" 44 | width = self.calculate_counter_width(addr, "R") 45 | sio.write(f" bit [{width - 1}:0] {counter_name};\n") 46 | 47 | self.verilog_counter.verilog_read_counter = sio 48 | 49 | def generate_write_counters(self, bar_number: int) -> None: 50 | """Generate write counter declarations for a specific BAR.""" 51 | write_addrs = [ 52 | data.address 53 | for data in VerilogData.get_all_instances() 54 | if data.operation == "W" and data.address is not None and data.bar == bar_number 55 | ] 56 | 57 | if not write_addrs: 58 | return 59 | 60 | sio = StringIO() 61 | unique_write_addrs = self.get_unique_sorted_addresses(write_addrs) 62 | for addr in unique_write_addrs: 63 | counter_name = f"W_C_{addr}" 64 | width = self.calculate_counter_width(addr, "W") 65 | sio.write(f" bit [{width - 1}:0] {counter_name};\n") 66 | 67 | self.verilog_counter.verilog_write_counter = sio 68 | 69 | def calculate_counter_width(self, address: str, operation: str) -> int: 70 | """Calculate required bit width for counter at a specific address.""" 71 | count = sum( 72 | 1 for data in VerilogData.get_all_instances() if data.address == address and data.operation == operation 73 | ) 74 | return max(1, (count).bit_length()) 75 | 76 | def reset_read_counters(self, bar_number: int) -> None: 77 | """Generate reset logic for read counters.""" 78 | read_addrs: list[str] = [ 79 | data.address 80 | for data in VerilogData.get_all_instances() 81 | if data.operation == "R" and data.address is not None and data.bar == bar_number 82 | ] 83 | 84 | if not read_addrs: 85 | return 86 | 87 | sio = StringIO() 88 | 89 | unique_read_addrs = self.get_unique_sorted_addresses(read_addrs) 90 | for addr in unique_read_addrs: 91 | counter_name = f"R_C_{addr}" 92 | sio.write(f" {counter_name} <= '0;\n") 93 | self.verilog_counter.verilog_reset_read_counter = sio 94 | 95 | def reset_write_counters(self, bar_number: int) -> None: 96 | """Generate reset logic for write counters.""" 97 | write_addrs: list[str] = [ 98 | data.address 99 | for data in VerilogData.get_all_instances() 100 | if data.operation == "W" and data.address is not None and data.bar == bar_number 101 | ] 102 | 103 | if not write_addrs: 104 | return 105 | 106 | sio = StringIO() 107 | unique_write_addrs = self.get_unique_sorted_addresses(write_addrs) 108 | for addr in unique_write_addrs: 109 | counter_name = f"W_C_{addr}" 110 | sio.write(f" {counter_name} <= '0;\n") 111 | self.verilog_counter.verilog_reset_write_counter = sio 112 | 113 | def generate_verilog_counter(self, bar_number: int) -> CounterEntry: 114 | """Generate Verilog code for counter module for a specific BAR.""" 115 | self.generate_read_counters(bar_number) 116 | self.generate_write_counters(bar_number) 117 | 118 | return self.verilog_counter 119 | 120 | def generate_reset_counter(self, bar_number: int) -> CounterEntry: 121 | """Generate reset logic for counters.""" 122 | self.reset_read_counters(bar_number) 123 | self.reset_write_counters(bar_number) 124 | 125 | return self.verilog_counter 126 | -------------------------------------------------------------------------------- /mmio/application/verilog/generators/response_logic.py: -------------------------------------------------------------------------------- 1 | """Generator module for Verilog logic operations.""" 2 | 3 | from io import StringIO 4 | 5 | from pydantic import Field 6 | 7 | from mmio.application.verilog.verilog_models import ( 8 | CounterEntry, 9 | LogicEntries, 10 | ROMEntry, 11 | VerilogGenerator, 12 | ) 13 | from mmio.core.logger import LoggerManager 14 | from mmio.domain.models.verilog_data import VerilogData 15 | 16 | logger = LoggerManager.get_logger(__name__) 17 | 18 | 19 | class ResponseLogicGenerator(VerilogGenerator): 20 | """Generate response logic for Verilog.""" 21 | 22 | verilog_data: VerilogData = Field(default_factory=VerilogData) 23 | 24 | verilog_logic: LogicEntries = Field( 25 | default_factory=lambda: LogicEntries( 26 | bar_number=0, 27 | verilog_read_cases=StringIO(), 28 | verilog_write_cases=StringIO(), 29 | ) 30 | ) 31 | 32 | def generate_read_logic(self, bar_number: int, rom_entry: ROMEntry, counter_entry: CounterEntry) -> None: 33 | """Generate read operation logic.""" 34 | read_addrs = { 35 | data.address: data.value 36 | for data in VerilogData.get_all_instances() 37 | if data.operation == "R" and data.address is not None and data.bar == bar_number 38 | } 39 | 40 | if not read_addrs: 41 | return 42 | 43 | sio = StringIO() 44 | sio.write(" if (drd_req_valid && read_addr_check(local_read_addr)) begin\n") 45 | sio.write(" case(local_read_addr)\n") 46 | 47 | default_value = self.verilog_data.get_default_values(bar_number)[0] 48 | for addr in sorted(read_addrs.keys()): 49 | rom_name = rom_entry.get_rom_name(addr, "R") 50 | counter_name = counter_entry.get_counter_name(addr, "R") 51 | 52 | count = sum(1 for data in VerilogData.get_all_instances() if data.address == addr and data.operation == "R") 53 | 54 | if count > 1: 55 | counter_width = (count).bit_length() 56 | sio.write(f" 20'h{addr}: begin\n") 57 | sio.write( 58 | f" {counter_name} <= ({counter_name} == {counter_width}'d{count - 1}) ? " 59 | ) 60 | sio.write(f"{counter_width}'d0 : {counter_name} + 1;\n") 61 | sio.write(f" rd_rsp_data <= {rom_name}[{counter_name}];\n") 62 | sio.write(" end\n") 63 | else: 64 | sio.write(f" 20'h{addr}: rd_rsp_data <= {rom_name}[0];\n") 65 | 66 | sio.write(f" default: rd_rsp_data <= 32'h{default_value};\n") 67 | sio.write(" endcase\n") 68 | sio.write(f" rd_rsp_data <= 32'h{default_value};\n") 69 | sio.write(" end\n") 70 | 71 | self.verilog_logic.verilog_read_cases = sio 72 | 73 | def generate_write_logic(self, bar_number: int, rom_entry: ROMEntry, counter_entry: CounterEntry) -> None: 74 | """Generate write operation logic.""" 75 | write_addrs = { 76 | data.address: data.value 77 | for data in VerilogData.get_all_instances() 78 | if data.operation == "W" and data.address is not None and data.bar == bar_number 79 | } 80 | 81 | if not write_addrs: 82 | return 83 | 84 | sio = StringIO() 85 | sio.write(" if (dwr_valid && write_addr_check(local_write_addr)) begin\n") 86 | sio.write(" case(local_write_addr)\n") 87 | 88 | default_value = self.verilog_data.get_default_values(bar_number)[1] 89 | for addr in sorted(write_addrs.keys()): 90 | rom_name = rom_entry.get_rom_name(addr, "W") 91 | counter_name = counter_entry.get_counter_name(addr, "W") 92 | 93 | count = sum(1 for data in VerilogData.get_all_instances() if data.address == addr and data.operation == "W") 94 | 95 | if count > 1: 96 | counter_width = (count).bit_length() 97 | sio.write(f" 20'h{addr}: begin\n") 98 | sio.write( 99 | f" {counter_name} <= ({counter_name} == {counter_width}'d{count - 1}) ? " 100 | ) 101 | sio.write(f"{counter_width}'d0 : {counter_name} + 1;\n") 102 | sio.write(f" {rom_name}[{counter_name}] <= dwr_data;\n") 103 | sio.write(" end\n") 104 | else: 105 | sio.write(f" 20'h{addr}: {rom_name}[0] <= dwr_data;\n") 106 | 107 | sio.write(f" default: wr_data_out <= 32'h{default_value};\n") 108 | sio.write(" endcase\n") 109 | sio.write(" end\n") 110 | sio.write(" end\n") 111 | sio.write(" end\n") 112 | self.verilog_logic.verilog_write_cases = sio 113 | 114 | def generate_verilog_logic(self, bar_number: int, rom_entry: ROMEntry, counter_entry: CounterEntry) -> LogicEntries: 115 | """Generate all Verilog logic for a specific BAR.""" 116 | self.generate_read_logic(bar_number, rom_entry, counter_entry) 117 | self.generate_write_logic(bar_number, rom_entry, counter_entry) 118 | return self.verilog_logic 119 | -------------------------------------------------------------------------------- /run.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal enabledelayedexpansion 3 | 4 | :: Start execution 5 | goto :main 6 | 7 | :: Function definitions 8 | :check_root_directory 9 | if not exist "setup.py" ( 10 | echo Error: Must be run from project root directory containing setup.py 11 | exit /b 1 12 | ) 13 | if not exist "mmio" ( 14 | echo Error: Must be run from project root directory containing mmio package 15 | exit /b 1 16 | ) 17 | goto :eof 18 | 19 | :: Function to initialize environment variables 20 | :init_environment 21 | set "BASE_DIR=%~dp0" 22 | cd /d "%BASE_DIR%" 23 | set "DEV_MODE=" 24 | if "%~1"=="--dev" set "DEV_MODE=1" 25 | set "IS_POWERSHELL=" 26 | if defined PSModulePath set "IS_POWERSHELL=1" 27 | goto :eof 28 | 29 | :: Function to verify and install Python if needed 30 | :check_python 31 | echo Checking Python installation... 32 | py --version >nul 2>&1 33 | if %errorlevel% neq 0 ( 34 | echo Python not found, attempting automatic installation... 35 | 36 | :: Try winget installation 37 | winget --version >nul 2>&1 38 | if %errorlevel% equ 0 ( 39 | echo Installing Python using winget... 40 | winget install Python.Python.3.13 --accept-source-agreements --accept-package-agreements 41 | if %errorlevel% equ 0 ( 42 | echo Python installed successfully 43 | :: Refresh environment variables 44 | refreshenv.cmd >nul 2>&1 45 | goto :eof 46 | ) 47 | ) 48 | 49 | echo. 50 | echo Error: Could not automatically install Python 51 | echo Please install Python 3.13 or later manually from: 52 | echo https://www.python.org/downloads/ 53 | echo Make sure to check "Add python.exe to PATH" during installation 54 | pause 55 | exit /b 1 56 | ) 57 | goto :eof 58 | 59 | :: Function to create/verify virtual environment 60 | :setup_venv 61 | if not exist "%BASE_DIR%.venv" ( 62 | echo Creating virtual environment... 63 | py -3 -m venv "%BASE_DIR%.venv" >nul 2>venv_error.tmp 64 | if %errorlevel% neq 0 ( 65 | type venv_error.tmp 66 | del venv_error.tmp 67 | echo Failed to create virtual environment 68 | pause 69 | exit /b 1 70 | ) 71 | del venv_error.tmp 72 | echo Virtual environment created 73 | ) else if not exist "%BASE_DIR%.venv\Scripts\python.exe" ( 74 | echo Virtual environment exists but appears invalid. Recreating... 75 | rmdir /s /q "%BASE_DIR%.venv" 76 | call :setup_venv 77 | exit /b %errorlevel% 78 | ) 79 | goto :eof 80 | 81 | :: Function to activate virtual environment 82 | :activate_venv 83 | echo Activating virtual environment... 84 | if defined IS_POWERSHELL ( 85 | powershell -NoProfile -ExecutionPolicy Bypass -Command "& '%BASE_DIR%.venv\Scripts\Activate.ps1'" >nul 2>activate_error.tmp 86 | ) else ( 87 | call "%BASE_DIR%.venv\Scripts\activate.bat" >nul 2>activate_error.tmp 88 | ) 89 | if %errorlevel% neq 0 ( 90 | type activate_error.tmp 91 | del activate_error.tmp 92 | echo Failed to activate virtual environment 93 | pause 94 | exit /b 1 95 | ) 96 | del activate_error.tmp 97 | set "PYTHON=%BASE_DIR%.venv\Scripts\python.exe" 98 | echo Virtual environment activated 99 | goto :eof 100 | 101 | :: Function to upgrade pip 102 | :upgrade_pip 103 | echo Upgrading pip... 104 | "%PYTHON%" -m pip install --upgrade pip >nul 2>pip_upgrade_error.tmp 105 | if %errorlevel% neq 0 ( 106 | type pip_upgrade_error.tmp 107 | del pip_upgrade_error.tmp 108 | echo Failed to upgrade pip 109 | pause 110 | exit /b 1 111 | ) 112 | del pip_upgrade_error.tmp 113 | echo Pip upgrade complete 114 | goto :eof 115 | 116 | :: Function to run setup.py 117 | :run_setup 118 | echo Running setup.py... 119 | if defined DEV_MODE ( 120 | echo Installing in development mode... 121 | "%PYTHON%" -m pip install -e ".[dev]" >nul 2>setup_error.tmp 122 | ) else ( 123 | "%PYTHON%" -m pip install -e "." >nul 2>setup_error.tmp 124 | ) 125 | if %errorlevel% neq 0 ( 126 | type setup_error.tmp 127 | del setup_error.tmp 128 | echo Failed to run setup.py 129 | pause 130 | exit /b 1 131 | ) 132 | del setup_error.tmp 133 | echo Setup complete 134 | goto :eof 135 | 136 | 137 | :: Function to run the application 138 | :run_application 139 | echo Running mmio package... 140 | "%PYTHON%" -m mmio 2>mmio_error.tmp 141 | if %errorlevel% neq 0 ( 142 | type mmio_error.tmp 143 | del mmio_error.tmp 144 | echo Failed to run mmio package 145 | exit /b 1 146 | ) 147 | del mmio_error.tmp 148 | echo Application ran successfully 149 | goto :eof 150 | 151 | :: Function to display activation instructions 152 | :show_activation_instructions 153 | echo. 154 | if defined IS_POWERSHELL ( 155 | echo To activate the virtual environment manually, run: %BASE_DIR%.venv\Scripts\Activate.ps1 156 | ) else ( 157 | echo To activate the virtual environment manually, run: %BASE_DIR%.venv\Scripts\activate.bat 158 | ) 159 | goto :eof 160 | 161 | :: Main execution flow 162 | :main 163 | call :check_root_directory 164 | if %errorlevel% neq 0 exit /b %errorlevel% 165 | 166 | call :init_environment %* 167 | if %errorlevel% neq 0 exit /b %errorlevel% 168 | 169 | call :check_python 170 | if %errorlevel% neq 0 exit /b %errorlevel% 171 | 172 | call :setup_venv 173 | if %errorlevel% neq 0 exit /b %errorlevel% 174 | 175 | call :activate_venv 176 | if %errorlevel% neq 0 exit /b %errorlevel% 177 | 178 | call :upgrade_pip 179 | if %errorlevel% neq 0 exit /b %errorlevel% 180 | 181 | call :run_setup 182 | if %errorlevel% neq 0 exit /b %errorlevel% 183 | 184 | call :run_application 185 | if %errorlevel% neq 0 exit /b %errorlevel% 186 | 187 | call :show_activation_instructions 188 | echo Installation completed successfully! 189 | goto :eof 190 | 191 | endlocal -------------------------------------------------------------------------------- /mmio/core/parse_logic.py: -------------------------------------------------------------------------------- 1 | """Patterns for MMIO parsing.""" 2 | 3 | from collections import OrderedDict 4 | from typing import Any 5 | 6 | from pydantic import BaseModel 7 | 8 | from mmio.core.logger import LoggerManager 9 | 10 | logger = LoggerManager.get_logger(__name__) 11 | 12 | 13 | class MMIOParseLogic(BaseModel): 14 | """Patterns for MMIO parsing. 15 | 16 | This class handles the parsing of MMIO log lines in the format: 17 | Operation Counter Timestamp Bar Address Value Value2 Final 18 | Example: W 2 298.823649 1 0xf70003fc 0x7a 0x0 0 19 | """ 20 | 21 | @staticmethod 22 | def address_offset_shift( 23 | address: str, 24 | offset: str, 25 | ) -> list[tuple[str, int]]: 26 | """Convert offset addresses to base address.""" 27 | offset_to_base: dict[str, str] = { 28 | offset: base 29 | for base, offset in { 30 | "0": "1230", 31 | "4": "5674", 32 | "8": "9ab8", 33 | "c": "defc", 34 | }.items() 35 | for offset in offset 36 | } 37 | 38 | shift_map: dict[int, str] = { 39 | 0: "048c", 40 | 8: "159d", 41 | 16: "26ae", 42 | 24: "37bf", 43 | } 44 | 45 | offset = offset.lower() 46 | base = offset_to_base.get(offset) 47 | 48 | base_addr = f"{address[:-1]}{base}" 49 | shift = next((k for k, v in shift_map.items() if offset in v), 0) 50 | 51 | return [(base_addr, shift)] 52 | 53 | @classmethod 54 | def align_register_to_offset( 55 | cls, 56 | address: str, 57 | value: str, 58 | ) -> tuple[str, str]: 59 | """Aligns register value based on address offset.""" 60 | last_char = address[-1] 61 | base_addr, shift_amount = cls.address_offset_shift(address[2:], last_char)[0] 62 | value_int = int(value, 16) 63 | hex_value = f"{(value_int << shift_amount) & 0xFFFFFFFF:08x}" 64 | return base_addr, hex_value 65 | 66 | @staticmethod 67 | def is_valid_hex(value: str) -> bool: 68 | """Check if a string is a valid hexadecimal value starting with 0x.""" 69 | try: 70 | if not value.startswith("0x"): 71 | return False 72 | int(value, 16) 73 | return True 74 | except ValueError: 75 | return False 76 | 77 | @staticmethod 78 | def is_valid_line(line: str) -> bool: 79 | """Check if line starts with W or R and has the correct format.""" 80 | return line.strip().startswith(("W", "R")) 81 | 82 | @staticmethod 83 | def is_write(line: str) -> bool: 84 | """Check if line is a write operation.""" 85 | return line.strip().startswith("W") 86 | 87 | @staticmethod 88 | def is_read(line: str) -> bool: 89 | """Check if line is a read operation.""" 90 | return line.strip().startswith("R") 91 | 92 | @staticmethod 93 | def bar_number( 94 | parts: list[str], 95 | base_addr: str, 96 | value: str, 97 | ) -> int: 98 | """Get the bar number from line parts with base address and value validation.""" 99 | return int(parts[3]) 100 | 101 | @staticmethod 102 | def validate_line_format(line: str) -> list[str]: 103 | """Validate line format and split into parts.""" 104 | if not MMIOParseLogic.is_valid_line(line): 105 | raise ValueError("Invalid line format - must start with W or R") 106 | 107 | parts = line.strip().split() 108 | if len(parts) != 8: 109 | raise ValueError("Invalid line format - expected 8 fields") 110 | 111 | for hex_value in [parts[4], parts[5]]: 112 | if not MMIOParseLogic.is_valid_hex(hex_value): 113 | raise ValueError(f"Invalid hex value: {hex_value}") 114 | 115 | return parts 116 | 117 | @staticmethod 118 | def process_address(address: str) -> tuple[str, str, int]: 119 | """Process address to get base address and shift amount.""" 120 | address = address[2:] 121 | if len(address) < 1: 122 | raise ValueError(f"Invalid address format: {address}") 123 | 124 | last_char = address[-1] 125 | base_addr, shift_amount = MMIOParseLogic.address_offset_shift( 126 | address, last_char 127 | )[0] 128 | 129 | return base_addr, address, shift_amount 130 | 131 | @staticmethod 132 | def create_mmio_data( 133 | parts: list[str], 134 | operation: str, 135 | bar: int, 136 | address: str, 137 | value: str, 138 | ) -> OrderedDict[str, Any]: 139 | """Create MMIO data dictionary from parsed components.""" 140 | try: 141 | return OrderedDict( 142 | [ 143 | ("operation", operation), 144 | ("counter", int(parts[1])), 145 | ("timestamp", float(parts[2])), 146 | ("bar_number", bar), 147 | ("address", address), 148 | ("value", value), 149 | ] 150 | ) 151 | 152 | except (ValueError, IndexError) as e: 153 | raise ValueError(f"Error creating MMIO data: {str(e)}") 154 | 155 | @staticmethod 156 | def parse_line(line: str) -> OrderedDict[str, Any]: 157 | """Parse a valid line into its components.""" 158 | parts = MMIOParseLogic.validate_line_format(line) 159 | operation = "W" if MMIOParseLogic.is_write(line) else "R" 160 | 161 | base_addr, address, shift_amount = MMIOParseLogic.process_address(parts[4]) 162 | bar = MMIOParseLogic.bar_number( 163 | parts, 164 | base_addr, 165 | parts[5], 166 | ) 167 | aligned_address, shifted_value = MMIOParseLogic.align_register_to_offset( 168 | parts[4], 169 | parts[5], 170 | ) 171 | 172 | return MMIOParseLogic.create_mmio_data( 173 | parts, 174 | operation, 175 | bar, 176 | aligned_address, 177 | shifted_value, 178 | ) 179 | -------------------------------------------------------------------------------- /mmio/application/verilog/generators/static_generator.py: -------------------------------------------------------------------------------- 1 | """Verilog module header generator.""" 2 | 3 | from io import StringIO 4 | 5 | from pydantic import Field 6 | 7 | from mmio.application.verilog.verilog_models import VerilogGenerator, VerilogStatic 8 | from mmio.config.verilog_settings import VerilogSettings 9 | from mmio.core.logger import LoggerManager 10 | 11 | logger = LoggerManager.get_logger(__name__) 12 | 13 | 14 | class StaticCodeGenerator(VerilogGenerator): 15 | """Verilog module static code generator. 16 | 17 | This class generates the Verilog module static code with all required ports 18 | for MMIO operations. It handles the generation of standard PCIe BAR 19 | implementation module static code. 20 | """ 21 | 22 | static_code: VerilogStatic = Field( 23 | default_factory=lambda: VerilogStatic( 24 | bar_number=0, 25 | verilog_header=StringIO(), 26 | verilog_state_machine_start=StringIO(), 27 | verilog_state_machine_end=StringIO(), 28 | ) 29 | ) 30 | 31 | verilog_settings: VerilogSettings = Field(default_factory=lambda: VerilogSettings.get_instance()) 32 | save_header: StringIO = Field(default_factory=StringIO) 33 | 34 | def generate_module_header(self) -> VerilogStatic: 35 | """Generate the Verilog module header with all ports.""" 36 | self.save_header.seek(0) 37 | self.save_header.truncate() 38 | 39 | self.save_header.write(f"module pcileech_bar_impl_{self.verilog_settings.module_header}_{self.bar_number}(\n") 40 | self.save_header.write(" input rst,\n") 41 | self.save_header.write(" input clk,\n") 42 | self.save_header.write(" // incoming BAR writes:\n") 43 | self.save_header.write(" input [31:0] wr_addr,\n") 44 | self.save_header.write(" input [3:0] wr_be,\n") 45 | self.save_header.write(" input [31:0] wr_data,\n") 46 | self.save_header.write(" input wr_valid,\n") 47 | self.save_header.write(" // incoming BAR reads:\n") 48 | self.save_header.write(" input [87:0] rd_req_ctx,\n") 49 | self.save_header.write(" input [31:0] rd_req_addr,\n") 50 | self.save_header.write(" input rd_req_valid,\n") 51 | self.save_header.write(" input [31:0] base_address_register,\n") 52 | self.save_header.write(" // outgoing BAR read replies:\n") 53 | self.save_header.write(" output logic [87:0] rd_rsp_ctx,\n") 54 | self.save_header.write(" output logic [31:0] rd_rsp_data,\n") 55 | self.save_header.write(" output logic rd_rsp_valid\n") 56 | self.save_header.write(");\n\n") 57 | self.save_header.write(" bit [87:0] drd_req_ctx;\n") 58 | self.save_header.write(" bit [31:0] drd_req_addr;\n") 59 | self.save_header.write(" bit drd_req_valid;\n") 60 | self.save_header.write("\n") 61 | self.save_header.write(" bit [31:0] dwr_addr;\n") 62 | self.save_header.write(" bit [31:0] dwr_data;\n") 63 | self.save_header.write(" bit dwr_valid;\n") 64 | self.save_header.write(" bit [31:0] wr_data_out;\n") 65 | self.save_header.write("\n") 66 | self.save_header.write(" wire [15:0] local_read_addr;\n") 67 | self.save_header.write(" wire [15:0] local_write_addr;\n") 68 | self.save_header.write("\n") 69 | self.save_header.write(" assign local_read_addr = ({drd_req_addr[31:24], drd_req_addr[23:16],\n") 70 | self.save_header.write(" drd_req_addr[15:8], drd_req_addr[7:0]} -\n") 71 | self.save_header.write(" (base_address_register & ~32'hFFF)) & 20'hFFFFF;\n") 72 | self.save_header.write("\n") 73 | self.save_header.write(" assign local_write_addr = ({dwr_addr[31:24], dwr_addr[23:16],\n") 74 | self.save_header.write(" dwr_addr[15:8], dwr_addr[7:0]} -\n") 75 | self.save_header.write(" (base_address_register & ~32'hFFF)) & 20'hFFFFF;\n") 76 | self.save_header.write("\n\n") 77 | 78 | self.static_code.verilog_header = StringIO(self.save_header.getvalue()) 79 | return self.static_code 80 | 81 | save_state_machine: StringIO = Field(default_factory=StringIO) 82 | 83 | def generate_state_machine_start(self) -> VerilogStatic: 84 | """Generate the Verilog state machine with all ports.""" 85 | self.save_state_machine.seek(0) 86 | self.save_state_machine.truncate() 87 | 88 | self.save_state_machine.write(" always_ff @(posedge clk) begin\n") 89 | self.save_state_machine.write(" if (rst) begin\n") 90 | self.save_state_machine.write(" rd_rsp_valid <= 1'b0;\n") 91 | 92 | self.static_code.verilog_state_machine_start = StringIO(self.save_state_machine.getvalue()) 93 | return self.static_code 94 | 95 | def generate_state_machine_end(self) -> VerilogStatic: 96 | """Generate the Verilog state machine end.""" 97 | self.save_state_machine.seek(0) 98 | self.save_state_machine.truncate() 99 | 100 | self.save_state_machine.write(" end else begin\n") 101 | self.save_state_machine.write(" drd_req_ctx <= rd_req_ctx;\n") 102 | self.save_state_machine.write(" drd_req_valid <= rd_req_valid;\n") 103 | self.save_state_machine.write(" dwr_valid <= wr_valid;\n") 104 | self.save_state_machine.write(" drd_req_addr <= rd_req_addr;\n") 105 | self.save_state_machine.write(" rd_rsp_ctx <= drd_req_ctx;\n") 106 | self.save_state_machine.write(" rd_rsp_valid <= drd_req_valid;\n") 107 | self.save_state_machine.write(" dwr_addr <= wr_addr;\n") 108 | self.save_state_machine.write(" dwr_data <= wr_data;\n") 109 | 110 | self.static_code.verilog_state_machine_end = StringIO(self.save_state_machine.getvalue()) 111 | return self.static_code 112 | -------------------------------------------------------------------------------- /mmio/application/cli/mmio.py: -------------------------------------------------------------------------------- 1 | """MMIO CLI app.""" 2 | 3 | from pathlib import Path 4 | 5 | import click 6 | from pydantic import BaseModel, ConfigDict, Field 7 | 8 | from mmio.application.cli.commands.mmio_select import SelectMMIOFileInputCLI 9 | from mmio.config.mmio_settings import MMIOSettings 10 | from mmio.core.logger import LoggerManager 11 | from mmio.infrastructure.file_handling.mmio_filemanager import MMIOFileManager 12 | 13 | logger = LoggerManager.get_logger(__name__) 14 | 15 | 16 | class MMIOCLIManager(BaseModel): 17 | """MMIO CLI manager. 18 | 19 | This class is responsible for managing the MMIO CLI application. 20 | Handles the MMIO settings, file selection, and MMIO file operations. 21 | Coordinates between the CLI interface and file management components. 22 | 23 | Attributes: 24 | mmio_select: CLI component for MMIO file selection 25 | settings: MMIO configuration settings 26 | file_manager: Manager for MMIO file operations 27 | selected_file: Currently selected MMIO file path 28 | 29 | """ 30 | 31 | model_config = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True) 32 | 33 | mmio_select: SelectMMIOFileInputCLI = Field(default_factory=SelectMMIOFileInputCLI) 34 | settings: MMIOSettings = Field(default_factory=MMIOSettings.get_instance) 35 | file_manager: MMIOFileManager = Field(default_factory=MMIOFileManager.get_instance) 36 | selected_file: Path | None = None 37 | 38 | def setup_cli(self) -> None: 39 | """Initialize MMIO CLI components and file manager.""" 40 | self._initialize_file_manager() 41 | self._setup_cli_integration() 42 | self.mmio_select.setup_commands() 43 | 44 | def _initialize_file_manager(self) -> None: 45 | """Initialize the file manager with settings.""" 46 | if self.settings.file_input_path: 47 | self.file_manager.input_manager.file_manager.path = self.settings.file_input_path 48 | logger.info(f"Initialized file manager with path: {self.settings.file_input_path}") 49 | 50 | def _setup_cli_integration(self) -> None: 51 | """Set up integration between CLI and file manager components.""" 52 | self.mmio_select.set_list_files_callback(self.list_available_files) 53 | 54 | self.mmio_select.add_option( 55 | param_decls=["--path-to-mmio-files"], 56 | type=click.Path(exists=True, file_okay=False, dir_okay=True), 57 | help="Set the MMIO files directory path", 58 | callback=self._set_path_callback, 59 | ) 60 | 61 | def _set_path_callback(self, ctx: click.Context, param: click.Parameter, value: str) -> None: 62 | """Callback for setting the MMIO files directory path.""" 63 | if not value: 64 | return 65 | try: 66 | path = Path(value) 67 | self.file_manager.input_manager.file_manager.path = path 68 | self.settings.file_input_path = path 69 | click.echo(f"Set MMIO files directory to: {path}") 70 | except Exception as e: 71 | logger.error(f"Error setting path: {e}") 72 | click.echo(f"Error setting path: {e}") 73 | 74 | def list_available_files(self) -> list[Path]: 75 | """List all available MMIO log files.""" 76 | try: 77 | files = self.file_manager.list_files() 78 | if not files: 79 | click.echo("No MMIO log files found in the input directory") 80 | return files 81 | except Exception as e: 82 | logger.error(f"Error listing files: {e}") 83 | click.echo(f"Error listing files: {e}") 84 | return [] 85 | 86 | def select_file(self, file_name: str) -> bool: 87 | """Select an MMIO file for processing.""" 88 | try: 89 | logger.info(f"Attempting to select file: {file_name}") 90 | logger.info( 91 | f"Current file manager state - input_path: " 92 | f"{self.file_manager.input_manager.file_manager.path}, input_file_name: " 93 | f"{self.file_manager.input_manager.file_manager.file_name}" 94 | ) 95 | 96 | self.file_manager.input_manager.file_manager.file_name = file_name 97 | logger.info(f"Set input_file_name to: {self.file_manager.input_manager.file_manager.file_name}") 98 | 99 | self.selected_file = self.file_manager.read_file() 100 | logger.info(f"Successfully read file: {self.selected_file}") 101 | 102 | self.mmio_select.selected_file = self.selected_file 103 | logger.info(f"Updated CLI selection state with file: {self.selected_file}") 104 | return True 105 | except (ValueError, FileNotFoundError) as e: 106 | logger.error(f"Error selecting file: {e}") 107 | logger.info( 108 | f"File manager state at error - input_path: " 109 | f"{self.file_manager.input_manager.file_manager.path}, input_file_name: " 110 | f"{self.file_manager.input_manager.file_manager.file_name}" 111 | ) 112 | click.echo(f"Error selecting file: {e}") 113 | return False 114 | 115 | def process_cli_selection(self) -> Path | None: 116 | """Process file selection through CLI interface.""" 117 | logger.info("Starting CLI file selection process") 118 | selected = self.mmio_select.prompt_user() 119 | 120 | if selected: 121 | logger.info(f"User selected file: {selected}") 122 | if self.select_file(selected.name): 123 | logger.info("File selection successful, proceeding with validation") 124 | if self.validate_and_process_file(): 125 | logger.info(f"File validation successful: {selected}") 126 | return selected 127 | else: 128 | logger.error("File validation failed") 129 | else: 130 | logger.error("File selection failed") 131 | else: 132 | logger.warning("No file was selected by user") 133 | 134 | return None 135 | 136 | def get_settings(self) -> MMIOSettings: 137 | """Get collected MMIO settings.""" 138 | return self.settings 139 | 140 | def get_selected_file(self) -> Path | None: 141 | """Get the currently selected file path.""" 142 | return self.selected_file 143 | 144 | def validate_and_process_file(self) -> bool: 145 | """Validate and prepare selected file for processing.""" 146 | if not self.selected_file: 147 | click.echo("No file selected for processing") 148 | return False 149 | 150 | try: 151 | self.file_manager.input_manager.file_manager.validate_file(self.selected_file) 152 | logger.info(f"File validated successfully: {self.selected_file}") 153 | return True 154 | except Exception as e: 155 | logger.error(f"Error validating file: {e}") 156 | click.echo(f"Error validating file: {e}") 157 | return False 158 | -------------------------------------------------------------------------------- /mmio/domain/services/orchestrators/main_orchestrator.py: -------------------------------------------------------------------------------- 1 | """Orchestrator to manage all modules.""" 2 | 3 | from pydantic import BaseModel, ConfigDict, Field 4 | 5 | from mmio.application.cli.app import AppLogic 6 | from mmio.application.cli.mmio import MMIOCLIManager 7 | from mmio.config.mmio_settings import MMIOSettings 8 | from mmio.config.verilog_settings import VerilogSettings 9 | from mmio.core.logger import LoggerManager 10 | from mmio.domain.models.verilog_data import VerilogData 11 | from mmio.infrastructure.file_handling.mmio_filemanager import MMIOFileManager 12 | from mmio.infrastructure.file_handling.verilog_filemanager import VerilogFileManager 13 | 14 | logger = LoggerManager.get_logger(__name__) 15 | 16 | 17 | class MainOrchestrator(BaseModel): 18 | """Main Orchestration. 19 | 20 | This class coordinates all application components and manages the main 21 | application flow. It handles initialization of components and ensures 22 | proper interaction between them. 23 | 24 | Attributes: 25 | verilog_settings: VerilogSettings to configure Verilog output 26 | mmio_settings: MMIOSettings to configure MMIO input 27 | app_logic: AppLogic to handle CLI interactions 28 | mmio_cli_manager: MMIOCLIManager to handle MMIO CLI interactions 29 | verilog_data: VerilogData to handle Verilog data 30 | mmio_file_manager: MMIOFileManager to handle MMIO file input 31 | verilog_file_manager: VerilogFileManager to handle Verilog file output 32 | 33 | """ 34 | 35 | model_config = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True) 36 | 37 | verilog_data: VerilogData = Field(default_factory=VerilogData) 38 | 39 | verilog_settings: VerilogSettings = Field(default_factory=VerilogSettings.get_instance) 40 | mmio_settings: MMIOSettings = Field(default_factory=MMIOSettings.get_instance) 41 | 42 | app_logic: AppLogic = Field(default_factory=AppLogic) 43 | mmio_cli_manager: MMIOCLIManager = Field(default_factory=MMIOCLIManager) 44 | 45 | mmio_file_manager: MMIOFileManager = Field(default_factory=MMIOFileManager.get_instance) 46 | verilog_file_manager: VerilogFileManager = Field(default_factory=VerilogFileManager.get_instance) 47 | 48 | results: list[dict[str, str | int | float | None]] = Field(default_factory=list) 49 | 50 | def _init_mmio_file_manager(self) -> None: 51 | logger.info(f"MMIO settings - file_input_path: {self.mmio_settings.file_input_path}") 52 | if self.mmio_settings.file_input_path: 53 | logger.info(f"Setting MMIO file manager input path: {self.mmio_settings.file_input_path}") 54 | self.mmio_file_manager.input_manager.file_manager.path = self.mmio_settings.file_input_path 55 | else: 56 | logger.warning("No MMIO input path set in settings") 57 | logger.info( 58 | f"Verilog settings - output_path: " 59 | f"{self.verilog_settings.file_output_path}, " 60 | f"output_name: {self.verilog_settings.file_output_name}" 61 | ) 62 | 63 | def _init_verilog_file_manager(self) -> None: 64 | """Initialize Verilog file manager with current settings.""" 65 | logger.info( 66 | f"Verilog settings - output_path: " 67 | f"{self.verilog_settings.file_output_path}, " 68 | f"output_name: {self.verilog_settings.file_output_name}" 69 | ) 70 | if self.verilog_settings.file_output_path: 71 | logger.info(f"Setting Verilog file manager output path: {self.verilog_settings.file_output_path}") 72 | self.verilog_file_manager.output_manager.file_manager.path = self.verilog_settings.file_output_path 73 | else: 74 | logger.warning("No Verilog output path set in settings") 75 | 76 | def _generate_output_filename(self) -> None: 77 | """Generate output filename if not already set.""" 78 | if self.verilog_settings.file_output_name: 79 | logger.info(f"Setting Verilog file manager output name: {self.verilog_settings.file_output_name}") 80 | self.verilog_file_manager.output_manager.file_manager.file_name = self.verilog_settings.file_output_name 81 | else: 82 | logger.info("Generating default Verilog output filename") 83 | self.verilog_file_manager.generate_output_filename() 84 | 85 | def _log_file_managers_state(self) -> None: 86 | """Log the state of the file managers.""" 87 | logger.info( 88 | f"Final MMIO file manager state - input_path: " 89 | f"{self.mmio_file_manager.input_manager.file_manager.path}, " 90 | f"input_file_name: {self.mmio_file_manager.input_manager.file_manager.file_name}" 91 | ) 92 | logger.info( 93 | f"Final Verilog file manager state - output_path: " 94 | f"{self.verilog_file_manager.output_manager.file_manager.path}, " 95 | f"output_file_name: {self.verilog_file_manager.output_manager.file_manager.file_name}" 96 | ) 97 | logger.info("File managers initialized") 98 | 99 | def _initialize_file_managers(self) -> None: 100 | """Initialize file managers with current settings.""" 101 | logger.info("Initializing file managers") 102 | 103 | self._init_mmio_file_manager() 104 | self._init_verilog_file_manager() 105 | self._generate_output_filename() 106 | self._log_file_managers_state() 107 | 108 | def initialize(self) -> None: 109 | """Initialize all application components. 110 | 111 | This method ensures all components are properly initialized and 112 | configured before the main application flow starts. 113 | """ 114 | try: 115 | logger.info("Initializing application components") 116 | 117 | self.app_logic.initialize() 118 | 119 | self.mmio_settings, self.verilog_settings = self.app_logic.get_settings() 120 | 121 | self._initialize_file_managers() 122 | 123 | logger.info("Application components initialized successfully") 124 | except Exception as e: 125 | logger.error(f"Failed to initialize components: {e}") 126 | raise 127 | 128 | def _run_cli_interface(self) -> None: 129 | """Run the CLI interface through AppLogic.""" 130 | self.app_logic.run() 131 | 132 | def _write_verilog_to_file(self) -> None: 133 | """Write the Verilog code to file.""" 134 | if self.verilog_settings.file_output_path: 135 | self.verilog_file_manager.output_manager.file_manager.path = self.verilog_settings.file_output_path 136 | logger.info(f"Writing Verilog output to {self.verilog_file_manager.output_manager.file_manager.path}") 137 | 138 | def generate_output(self) -> None: 139 | """Generate Verilog output from processed results.""" 140 | try: 141 | logger.info("Generating Verilog output") 142 | 143 | if not self.verilog_settings.file_output_name: 144 | self._generate_output_filename() 145 | 146 | self._write_verilog_to_file() 147 | 148 | logger.info("Verilog output generated successfully") 149 | except Exception as e: 150 | logger.error(f"Failed to generate output: {e}") 151 | raise 152 | 153 | def _process_generated_verilog(self) -> None: 154 | """Process the generated Verilog code.""" 155 | if self.app_logic.orchestrator: 156 | self.generate_output() 157 | 158 | def run(self) -> None: 159 | """Execute the complete application workflow. 160 | 161 | This method coordinates the main application flow: 162 | 1. Runs the CLI interface for user interaction 163 | 2. Processes selected files 164 | 3. Generates output 165 | """ 166 | try: 167 | logger.info("Starting application workflow") 168 | 169 | self._run_cli_interface() 170 | 171 | self._process_generated_verilog() 172 | 173 | logger.info("Application workflow completed successfully") 174 | except Exception as e: 175 | logger.error(f"Application workflow failed: {e}") 176 | raise 177 | -------------------------------------------------------------------------------- /mmio/application/cli/commands/log_settings.py: -------------------------------------------------------------------------------- 1 | """CLI interface for managing logging settings.""" 2 | 3 | from pathlib import Path 4 | from typing import Any 5 | 6 | import click 7 | from click import Argument, Group, Option 8 | from click import Command as ClickCommand 9 | from pydantic import BaseModel, ConfigDict, Field 10 | 11 | from mmio.config.log_settings import LOG_LEVELS, LogSettings 12 | from mmio.core.logger import LoggerManager 13 | from mmio.infrastructure.cli.base import CLIBase, CLICommand 14 | 15 | logger = LoggerManager.get_logger(__name__) 16 | 17 | 18 | class LogSettingsCLI(BaseModel): 19 | """CLI interface for managing logging settings.""" 20 | 21 | model_config = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True) 22 | 23 | cli_base: CLIBase = Field(default_factory=CLIBase) 24 | cli_commands: CLICommand = Field(default_factory=lambda: CLICommand(name="log")) 25 | settings: LogSettings = Field(default_factory=LogSettings.load) 26 | 27 | def model_post_init(self, __context: Any) -> None: 28 | """Initialize after model creation.""" 29 | self.cli_commands.group = self.log_group 30 | self.cli_commands.help = "Manage logging settings" 31 | self.cli_commands.epilog = "Configure logging behavior" 32 | self.cli_commands.short_help = "Logging settings" 33 | self.cli_commands.options_metavar = "[OPTIONS]" 34 | self.cli_commands.add_help_option = True 35 | self.cli_commands.no_args_is_help = False 36 | self.cli_commands.hidden = False 37 | self.cli_commands.deprecated = False 38 | self._setup_default_options() 39 | 40 | @property 41 | def log_group(self) -> Group: 42 | """Get the log settings group.""" 43 | return Group(name="log") 44 | 45 | @property 46 | def log_name(self) -> str: 47 | """Get the log settings name.""" 48 | return "log" 49 | 50 | def _setup_default_options(self) -> None: 51 | """Set up default CLI options.""" 52 | self.add_option( 53 | param_decls=["--set-level"], 54 | type=click.Choice(list(LOG_LEVELS.keys()), case_sensitive=False), 55 | help="Set the logging level", 56 | callback=self._set_level_callback, 57 | ) 58 | self.add_option( 59 | param_decls=["--enable-file-logging"], 60 | type=click.Path(dir_okay=False, path_type=Path), 61 | help="Enable logging to file", 62 | callback=self._set_file_callback, 63 | ) 64 | self.add_option( 65 | param_decls=["--disable-file-logging"], 66 | is_flag=True, 67 | help="Disable file logging", 68 | callback=self._disable_file_callback, 69 | ) 70 | self.add_option( 71 | param_decls=["--show-options"], 72 | is_flag=True, 73 | help="Show current logging settings", 74 | callback=self._show_settings_callback, 75 | ) 76 | 77 | def add_command(self, command: ClickCommand) -> None: 78 | """Add a command to the CLI.""" 79 | self.cli_base.add_group(group=self.log_group) 80 | self.cli_base.add_command(command=command) 81 | 82 | def add_argument(self, *args: Any, **kwargs: Any) -> None: 83 | """Add an argument to the CLI.""" 84 | self.cli_commands.params.append(Argument(*args, **kwargs)) 85 | 86 | def add_option(self, *args: Any, **kwargs: Any) -> None: 87 | """Add an option to the CLI.""" 88 | option = Option(*args, **kwargs) 89 | self.cli_commands.add_parameter(option) 90 | 91 | def add_help_option(self) -> None: 92 | """Add a help option to the CLI.""" 93 | self.cli_commands.add_help_option = True 94 | 95 | def add_group(self, group: Group) -> None: 96 | """Add a group to the CLI.""" 97 | self.cli_base.add_group(group=group) 98 | 99 | def setup_commands(self) -> None: 100 | """Set up all commands for the CLI.""" 101 | self.cli_base.setup_commands() 102 | 103 | def _set_level_callback(self, ctx: click.Context, param: click.Parameter, value: str) -> None: 104 | """Callback for setting the log level.""" 105 | if not value: 106 | return 107 | try: 108 | self.settings.level = value.upper() 109 | self.settings.save() 110 | self.settings.apply() 111 | click.echo(f"Log level set to: {value.upper()}") 112 | except Exception as e: 113 | logger.error(f"Failed to set log level: {e}") 114 | click.echo(f"Error: {e}") 115 | 116 | def _set_file_callback(self, ctx: click.Context, param: click.Parameter, value: Path) -> None: 117 | """Callback for enabling file logging.""" 118 | if not value: 119 | return 120 | try: 121 | self.settings.file_enabled = True 122 | self.settings.log_file = value 123 | self.settings.save() 124 | self.settings.apply() 125 | click.echo(f"File logging enabled: {value}") 126 | except Exception as e: 127 | logger.error(f"Failed to enable file logging: {e}") 128 | click.echo(f"Error: {e}") 129 | 130 | def _disable_file_callback(self, ctx: click.Context, param: click.Parameter, value: bool) -> None: 131 | """Callback for disabling file logging.""" 132 | if not value: 133 | return 134 | try: 135 | self.settings.file_enabled = False 136 | self.settings.log_file = None 137 | self.settings.save() 138 | self.settings.apply() 139 | click.echo("File logging disabled") 140 | except Exception as e: 141 | logger.error(f"Failed to disable file logging: {e}") 142 | click.echo(f"Error: {e}") 143 | 144 | def _show_settings_callback(self, ctx: click.Context, param: click.Parameter, value: bool) -> None: 145 | """Callback for showing current settings.""" 146 | if not value: 147 | return 148 | click.echo("\nCurrent Logging Settings:") 149 | click.echo(f"Level: {self.settings.level}") 150 | click.echo(f"File Logging: {'Enabled' if self.settings.file_enabled else 'Disabled'}") 151 | if self.settings.file_enabled and self.settings.log_file: 152 | click.echo(f"Log File: {self.settings.log_file}") 153 | click.echo(f"Format: {self.settings.format}") 154 | click.echo(f"Date Format: {self.settings.date_format}") 155 | 156 | def prompt_user(self) -> None: 157 | """Prompt the user for logging settings.""" 158 | click.echo("\nLogging Settings Configuration") 159 | click.echo("===========================") 160 | 161 | levels = list(LOG_LEVELS.keys()) 162 | click.echo("\nAvailable log levels:") 163 | for idx, level in enumerate(levels, 1): 164 | click.echo(f"{idx}. {level}") 165 | 166 | try: 167 | level_idx = click.prompt( 168 | "Select log level (number)", 169 | type=click.IntRange(1, len(levels)), 170 | default=levels.index(self.settings.level) + 1, 171 | ) 172 | self.settings.level = levels[level_idx - 1] 173 | 174 | if click.confirm("Enable file logging?", default=self.settings.file_enabled): 175 | file_path = click.prompt( 176 | "Log file path", 177 | type=click.Path(dir_okay=False, path_type=Path), 178 | default=self.settings.log_file or Path("mmio.log"), 179 | ) 180 | self.settings.file_enabled = True 181 | self.settings.log_file = file_path 182 | else: 183 | self.settings.file_enabled = False 184 | self.settings.log_file = None 185 | 186 | self.settings.save() 187 | self.settings.apply() 188 | click.echo("\nLogging settings updated successfully") 189 | 190 | except click.Abort: 191 | click.echo("\nConfiguration cancelled") 192 | except Exception as e: 193 | logger.error(f"Error configuring logging: {e}") 194 | click.echo(f"\nError: {e}") 195 | -------------------------------------------------------------------------------- /mmio/application/verilog/generators/rom.py: -------------------------------------------------------------------------------- 1 | """ROM module.""" 2 | 3 | from io import StringIO 4 | 5 | from pydantic import Field 6 | 7 | from mmio.application.verilog.verilog_models import ROMEntry, VerilogGenerator 8 | from mmio.core.logger import LoggerManager 9 | from mmio.domain.models.verilog_data import VerilogData 10 | 11 | logger = LoggerManager.get_logger(__name__) 12 | 13 | 14 | class ROMGenerator(VerilogGenerator): 15 | """ROM module. 16 | 17 | This class generates ROM structures for MMIO operations. 18 | Each unique address in a BAR gets its own ROM with a bit width determined by 19 | the maximum value seen at that address. 20 | 21 | ROM definitions and value assignments are stored separately: 22 | - Definitions go into ROMEntry.verilog_read_rom/verilog_write_rom 23 | - Value assignments can be accessed via get_rom_assignments() 24 | """ 25 | 26 | verilog_data: VerilogData = Field(default_factory=VerilogData) 27 | 28 | verilog_rom: ROMEntry = Field( 29 | default_factory=lambda: ROMEntry( 30 | bar_number=0, 31 | verilog_read_rom=StringIO(), 32 | verilog_write_rom=StringIO(), 33 | verilog_read_rom_init=StringIO(), 34 | verilog_write_rom_init=StringIO(), 35 | ) 36 | ) 37 | 38 | read_assignments: StringIO = Field(default_factory=StringIO) 39 | write_assignments: StringIO = Field(default_factory=StringIO) 40 | read_rom_init: StringIO = Field(default_factory=StringIO) 41 | write_rom_init: StringIO = Field(default_factory=StringIO) 42 | 43 | def get_rom_values_for_address(self, bar: int, address: str, operation: str) -> list[str]: 44 | """Get all values for a specific address and operation. 45 | 46 | Args: 47 | bar: BAR number 48 | address: Address to get values for 49 | operation: "R" for read values, "W" for write values 50 | 51 | Returns: 52 | List of values seen at this address for this operation 53 | 54 | """ 55 | return [ 56 | data.value 57 | for data in VerilogData.get_all_instances() 58 | if (data.bar == bar and data.address == address and data.operation == operation and data.value is not None) 59 | ] 60 | 61 | def generate_read_roms(self, bar_number: int) -> None: 62 | """Generate read ROM declarations for a specific BAR.""" 63 | logger.info("Creating new read ROM") 64 | self.verilog_rom.verilog_read_rom.seek(0) 65 | self.verilog_rom.verilog_read_rom.truncate() 66 | self.read_assignments = StringIO() 67 | 68 | address_widths = self.verilog_data.get_bar_address_bit_widths(bar_number) 69 | read_addrs = [ 70 | data.address 71 | for data in VerilogData.get_all_instances() 72 | if data.operation == "R" and data.address is not None and data.bar == bar_number 73 | ] 74 | unique_read_addrs = self.get_unique_sorted_addresses(read_addrs) 75 | 76 | for address in unique_read_addrs: 77 | bit_width = address_widths.get(address, 0) 78 | values = self.get_rom_values_for_address(bar_number, address, "R") 79 | if not values: 80 | continue 81 | 82 | rom_name = f"R_{address}" 83 | size = len(values) 84 | 85 | self.verilog_rom.verilog_read_rom.write(f" bit [{bit_width - 1}:0] {rom_name} [0:{size - 1}];\n") 86 | 87 | def generate_write_roms(self, bar_number: int) -> None: 88 | """Generate write ROM declarations for a specific BAR.""" 89 | logger.info("Creating new write ROM") 90 | self.verilog_rom.verilog_write_rom.seek(0) 91 | self.verilog_rom.verilog_write_rom.truncate() 92 | self.write_assignments = StringIO() 93 | 94 | address_widths = self.verilog_data.get_bar_address_bit_widths(bar_number) 95 | write_addrs = [ 96 | data.address 97 | for data in VerilogData.get_all_instances() 98 | if data.operation == "W" and data.address is not None and data.bar == bar_number 99 | ] 100 | unique_write_addrs = self.get_unique_sorted_addresses(write_addrs) 101 | 102 | for address in unique_write_addrs: 103 | bit_width = address_widths.get(address, 0) 104 | values = self.get_rom_values_for_address(bar_number, address, "W") 105 | if not values: 106 | continue 107 | 108 | rom_name = f"W_{address}" 109 | size = len(values) 110 | 111 | self.verilog_rom.verilog_write_rom.write(f" bit [{bit_width - 1}:0] {rom_name} [0:{size - 1}];\n") 112 | 113 | def get_address_value_pairs(self, bar_number: int, operation: str) -> dict[str, list[str]]: 114 | """Get all address-value pairs for a given BAR and operation. 115 | 116 | Args: 117 | bar_number: BAR number to get values for 118 | operation: 'R' for read values, 'W' for write values 119 | 120 | Returns: 121 | Dictionary mapping addresses to their values (including duplicates) 122 | 123 | """ 124 | address_values: dict[str, list[str]] = {} 125 | for data in VerilogData.get_all_instances(): 126 | if ( 127 | data.bar == bar_number 128 | and data.operation == operation 129 | and data.address is not None 130 | and data.value is not None 131 | ): 132 | if data.address not in address_values: 133 | address_values[data.address] = [] 134 | address_values[data.address].append(data.value) 135 | return address_values 136 | 137 | def initialize_read_roms(self, bar_number: int) -> None: 138 | """Initialize read ROM values.""" 139 | logger.info("Creating new read ROM initialization") 140 | self.verilog_rom.verilog_read_rom_init.seek(0) 141 | self.verilog_rom.verilog_read_rom_init.truncate() 142 | 143 | address_values = self.get_address_value_pairs(bar_number, "R") 144 | logger.info(f"Found values for addresses in BAR {bar_number}: {address_values.keys()}") 145 | 146 | for address, values in address_values.items(): 147 | rom_name = f"R_{address}" 148 | logger.debug(f"Initializing read ROM {rom_name} with {len(values)} values") 149 | for i, value in enumerate(values): 150 | self.verilog_rom.verilog_read_rom_init.write(f" {rom_name}[{i}] <= 32'h{value};\n") 151 | self.verilog_rom.verilog_read_rom_init.write("\n") 152 | self.verilog_rom.verilog_read_rom_init.seek(0) 153 | 154 | def initialize_write_roms(self, bar_number: int) -> None: 155 | """Initialize write ROM values.""" 156 | logger.info("Creating new write ROM initialization") 157 | self.verilog_rom.verilog_write_rom_init.seek(0) 158 | self.verilog_rom.verilog_write_rom_init.truncate() 159 | 160 | address_values = self.get_address_value_pairs(bar_number, "W") 161 | logger.info(f"Found values for addresses in BAR {bar_number}: {address_values.keys()}") 162 | 163 | for address, values in address_values.items(): 164 | rom_name = f"W_{address}" 165 | logger.debug(f"Initializing write ROM {rom_name} with {len(values)} values") 166 | for i, value in enumerate(values): 167 | self.verilog_rom.verilog_write_rom_init.write(f" {rom_name}[{i}] <= 32'h{value};\n") 168 | self.verilog_rom.verilog_write_rom_init.write("\n") 169 | self.verilog_rom.verilog_write_rom_init.seek(0) 170 | 171 | def generate_rom_structure(self, bar_number: int) -> None: 172 | """Generate all ROM declarations and assignments for a specific BAR.""" 173 | logger.info("Generating ROM structure") 174 | self.generate_read_roms(bar_number) 175 | self.generate_write_roms(bar_number) 176 | 177 | def generate_rom_init(self, bar_number: int) -> None: 178 | """Generate all ROM initializations for a specific BAR.""" 179 | logger.info("Generating ROM initialization") 180 | self.initialize_read_roms(bar_number) 181 | self.initialize_write_roms(bar_number) 182 | 183 | def get_rom_assignments(self, operation: str = "R") -> str: 184 | """Get ROM value assignments for use in state machine. 185 | 186 | Args: 187 | operation: "R" for read assignments, "W" for write assignments 188 | 189 | Returns: 190 | String containing all ROM value assignments 191 | 192 | """ 193 | if operation == "R": 194 | return self.read_assignments.getvalue() 195 | elif operation == "W": 196 | return self.write_assignments.getvalue() 197 | else: 198 | raise ValueError("Operation must be 'R' or 'W'") 199 | -------------------------------------------------------------------------------- /input/mmio/test.trace: -------------------------------------------------------------------------------- 1 | # tracer: mmiotrace 2 | # 3 | # entries-in-buffer/entries-written: 32536/55168 #P:1 4 | # 5 | # _-----=> irqs-off/BH-disabled 6 | # / _----=> need-resched 7 | # | / _---=> hardirq/softirq 8 | # || / _--=> preempt-depth 9 | # ||| / _-=> migrate-disable 10 | # |||| / delay 11 | # TASK-PID CPU# ||||| TIMESTAMP FUNCTION 12 | # | | | ||||| | | 13 | R 2 298.823649 1 0xf7000000 0x0 0x0 0 14 | R 2 298.823655 1 0xf7000000 0x0 0x0 0 15 | R 2 298.823657 1 0xf7000000 0x0 0x0 0 16 | R 2 298.823663 1 0xf7000000 0x0 0x0 0 17 | R 2 298.823665 1 0xf7000004 0x8 0x0 0 18 | R 2 298.823671 1 0xf7000004 0x6 0x0 0 19 | R 2 298.823673 1 0xf7000004 0x5 0x0 0 20 | R 4 298.823676 1 0xf7000004 0x8 0x0 0 21 | R 2 298.823678 1 0xf7000008 0x9 0x0 0 22 | R 2 298.823684 1 0xf7000008 0xA 0x0 0 23 | R 2 298.823686 1 0xf7000008 0xB 0x0 0 24 | R 2 298.823692 1 0xf7000008 0xC 0x0 0 25 | R 2 298.823694 1 0xf700000C 0xD 0x0 0 26 | R 2 298.823697 1 0xf700000C 0xE 0x0 0 27 | R 2 298.823702 1 0xf700000C 0xF 0x0 0 28 | R 2 298.823705 1 0xf700000C 0x10 0x0 0 29 | R 2 298.823707 1 0xf7000010 0x11 0x0 0 30 | R 2 298.823713 1 0xf7000010 0x12 0x0 0 31 | R 4 298.823715 1 0xf7000010 0x13 0x0 0 32 | R 2 298.823718 1 0xf7000010 0x14 0x0 0 33 | R 2 298.823724 1 0xf7000014 0x15 0x0 0 34 | R 2 298.823726 1 0xf7000014 0x16 0x0 0 35 | R 2 298.823732 1 0xf7000014 0x17 0x0 0 36 | R 2 298.823734 1 0xf7000014 0x18 0x0 0 37 | R 2 298.823740 1 0xf7000018 0x19 0x0 0 38 | R 4 298.823742 1 0xf7000018 0x1A 0x0 0 39 | R 2 298.823748 1 0xf7000018 0x1B 0x0 0 40 | R 2 298.823750 1 0xf7000018 0x1C 0x0 0 41 | R 2 298.823756 1 0xf700001C 0x1D 0x0 0 42 | R 4 298.823758 1 0xf700001C 0x1E 0x0 0 43 | R 2 298.823764 1 0xf700001C 0x1F 0x0 0 44 | R 2 298.823766 1 0xf700001C 0x20 0x0 0 45 | R 2 298.823772 1 0xf7000020 0x21 0x0 0 46 | R 4 298.823774 1 0xf7000020 0x22 0x0 0 47 | R 2 298.823780 1 0xf7000020 0x23 0x0 0 48 | R 2 298.823782 1 0xf7000020 0x24 0x0 0 49 | R 2 298.823788 1 0xf7000024 0x25 0x0 0 50 | R 4 298.823790 1 0xf7000024 0x26 0x0 0 51 | R 2 298.823796 1 0xf7000024 0x27 0x0 0 52 | R 2 298.823798 1 0xf7000024 0x28 0x0 0 53 | W 2 298.823649 1 0xf7000000 0x0 0x0 0 54 | W 2 298.823655 1 0xf7000000 0x0 0x0 0 55 | W 2 298.823657 1 0xf7000000 0x0 0x0 0 56 | W 2 298.823663 1 0xf7000000 0x0 0x0 0 57 | W 2 298.823665 1 0xf7000004 0x5 0x0 0 58 | W 2 298.823671 1 0xf7000004 0x6 0x0 0 59 | W 2 298.823673 1 0xf7000004 0x7 0x0 0 60 | W 4 298.823676 1 0xf7000004 0x8 0x0 0 61 | W 2 298.823678 1 0xf7000008 0x9 0x0 0 62 | W 2 298.823684 1 0xf7000008 0xA 0x0 0 63 | W 2 298.823686 1 0xf7000008 0xB 0x0 0 64 | W 2 298.823692 1 0xf7000008 0xC 0x0 0 65 | W 2 298.823694 1 0xf700000C 0xD 0x0 0 66 | W 2 298.823697 1 0xf700000C 0xE 0x0 0 67 | W 2 298.823702 1 0xf700000C 0xF 0x0 0 68 | W 2 298.823705 1 0xf700000C 0x10 0x0 0 69 | W 2 298.823707 1 0xf7000010 0x11 0x0 0 70 | W 2 298.823713 1 0xf7000010 0x12 0x0 0 71 | W 4 298.823715 1 0xf7000010 0x13 0x0 0 72 | W 2 298.823718 1 0xf7000010 0x14 0x0 0 73 | W 2 298.823724 1 0xf7000014 0x15 0x0 0 74 | W 2 298.823726 1 0xf7000014 0x16 0x0 0 75 | W 2 298.823732 1 0xf7000014 0x17 0x0 0 76 | W 2 298.823734 1 0xf7000014 0x18 0x0 0 77 | W 2 298.823740 1 0xf7000018 0x19 0x0 0 78 | W 4 298.823742 1 0xf7000018 0x1A 0x0 0 79 | W 2 298.823748 1 0xf7000018 0x1B 0x0 0 80 | W 2 298.823750 1 0xf7000018 0x1C 0x0 0 81 | W 2 298.823756 1 0xf700001C 0x1D 0x0 0 82 | W 4 298.823758 1 0xf700001C 0x1E 0x0 0 83 | W 2 298.823764 1 0xf700001C 0x1F 0x0 0 84 | W 2 298.823766 1 0xf700001C 0x20 0x0 0 85 | W 2 298.823772 1 0xf7000020 0x21 0x0 0 86 | W 4 298.823774 1 0xf7000020 0x22 0x0 0 87 | W 2 298.823780 1 0xf7000020 0x23 0x0 0 88 | W 2 298.823782 1 0xf7000020 0x24 0x0 0 89 | W 2 298.823788 1 0xf7000024 0x25 0x0 0 90 | W 4 298.823790 1 0xf7000024 0x26 0x0 0 91 | W 2 298.823796 1 0xf7000024 0x27 0x0 0 92 | W 2 298.823798 1 0xf7000024 0x28 0x0 0 93 | R 2 298.823649 1 0xf7000000 0x0 0x0 0 94 | R 2 298.823655 1 0xf7000000 0x0 0x0 0 95 | R 2 298.823657 1 0xf7000000 0x0 0x0 0 96 | R 2 298.823663 1 0xf7000000 0x0 0x0 0 97 | R 2 298.823665 1 0xf7000004 0x5 0x0 0 98 | R 2 298.823671 1 0xf7000004 0x6 0x0 0 99 | R 2 298.823673 1 0xf7000004 0x7 0x0 0 100 | R 4 298.823676 1 0xf7000004 0x8 0x0 0 101 | R 2 298.823678 1 0xf7000008 0x9 0x0 0 102 | R 2 298.823684 1 0xf7000008 0xA 0x0 0 103 | R 2 298.823686 1 0xf7000008 0xB 0x0 0 104 | R 2 298.823692 1 0xf7000008 0xC 0x0 0 105 | R 2 298.823694 1 0xf700000C 0xD 0x0 0 106 | R 2 298.823697 1 0xf700000C 0xE 0x0 0 107 | R 2 298.823702 1 0xf700000C 0xF 0x0 0 108 | R 2 298.823705 1 0xf700000C 0x10 0x0 0 109 | R 2 298.823707 1 0xf7000010 0x11 0x0 0 110 | R 2 298.823713 1 0xf7000010 0x12 0x0 0 111 | R 4 298.823715 1 0xf7000010 0x13 0x0 0 112 | R 2 298.823718 1 0xf7000010 0x14 0x0 0 113 | R 2 298.823724 1 0xf7000014 0x15 0x0 0 114 | R 2 298.823726 1 0xf7000014 0x16 0x0 0 115 | R 2 298.823732 1 0xf7000014 0x17 0x0 0 116 | R 2 298.823734 1 0xf7000014 0x18 0x0 0 117 | R 2 298.823740 1 0xf7000018 0x19 0x0 0 118 | R 4 298.823742 1 0xf7000018 0x1A 0x0 0 119 | R 2 298.823748 1 0xf7000018 0x1B 0x0 0 120 | R 2 298.823750 1 0xf7000018 0x1C 0x0 0 121 | R 2 298.823756 1 0xf700001C 0x1D 0x0 0 122 | R 4 298.823758 1 0xf700001C 0x1E 0x0 0 123 | R 2 298.823764 1 0xf700001C 0x1F 0x0 0 124 | R 2 298.823766 1 0xf700001C 0x20 0x0 0 125 | R 2 298.823772 1 0xf7000020 0x21 0x0 0 126 | R 4 298.823774 1 0xf7000020 0x22 0x0 0 127 | R 2 298.823780 1 0xf7000020 0x23 0x0 0 128 | R 2 298.823782 1 0xf7000020 0x24 0x0 0 129 | R 2 298.823788 1 0xf7000024 0x25 0x0 0 130 | R 4 298.823790 1 0xf7000024 0x26 0x0 0 131 | R 2 298.823796 1 0xf7000024 0x27 0x0 0 132 | R 2 298.823798 1 0xf7000024 0x28 0x0 0 133 | W 2 298.823649 1 0xf7000000 0x0 0x0 0 134 | W 2 298.823655 1 0xf7000000 0x0 0x0 0 135 | W 2 298.823657 1 0xf7000000 0x0 0x0 0 136 | W 2 298.823663 1 0xf7000000 0x0 0x0 0 137 | W 2 298.823665 1 0xf7000004 0x5 0x0 0 138 | W 2 298.823671 1 0xf7000004 0x6 0x0 0 139 | W 2 298.823673 1 0xf7000004 0x7 0x0 0 140 | W 4 298.823676 1 0xf7000004 0x8 0x0 0 141 | W 2 298.823678 1 0xf7000008 0x9 0x0 0 142 | W 2 298.823684 1 0xf7000008 0xA 0x0 0 143 | W 2 298.823686 1 0xf7000008 0xB 0x0 0 144 | W 2 298.823692 1 0xf7000008 0xC 0x0 0 145 | W 2 298.823694 1 0xf700000C 0xD 0x0 0 146 | W 2 298.823697 1 0xf700000C 0xE 0x0 0 147 | W 2 298.823702 1 0xf700000C 0xF 0x0 0 148 | W 2 298.823705 1 0xf700000C 0x10 0x0 0 149 | W 2 298.823707 1 0xf7000010 0x11 0x0 0 150 | W 2 298.823713 1 0xf7000010 0x12 0x0 0 151 | W 4 298.823715 1 0xf7000010 0x13 0x0 0 152 | W 2 298.823718 1 0xf7000010 0x14 0x0 0 153 | W 2 298.823724 1 0xf7000014 0x15 0x0 0 154 | W 2 298.823726 1 0xf7000014 0x16 0x0 0 155 | W 2 298.823732 1 0xf7000014 0x17 0x0 0 156 | W 2 298.823734 1 0xf7000014 0x18 0x0 0 157 | W 2 298.823740 1 0xf7000018 0x19 0x0 0 158 | W 4 298.823742 1 0xf7000018 0x1A 0x0 0 159 | W 2 298.823748 1 0xf7000018 0x1B 0x0 0 160 | W 2 298.823750 1 0xf7000018 0x1C 0x0 0 161 | W 2 298.823756 1 0xf700001C 0x1D 0x0 0 162 | W 4 298.823758 1 0xf700001C 0x1E 0x0 0 163 | W 2 298.823764 1 0xf700001C 0x1F 0x0 0 164 | W 2 298.823766 1 0xf700001C 0x20 0x0 0 165 | W 2 298.823772 1 0xf7000020 0x21 0x0 0 166 | W 4 298.823774 1 0xf7000020 0x22 0x0 0 167 | W 2 298.823780 1 0xf7000020 0x23 0x0 0 168 | W 2 298.823782 1 0xf7000020 0x24 0x0 0 169 | W 2 298.823788 1 0xf7000024 0x25 0x0 0 170 | W 4 298.823790 1 0xf7000024 0x26 0x0 0 171 | W 2 298.823796 1 0xf7000024 0x27 0x0 0 172 | W 2 298.823798 1 0xf7000024 0x28 0x0 0 173 | W 2 298.823705 2 0xf7000030 0xFFFFFFFF 0x0 0 174 | W 2 298.823707 2 0xf7000031 0xEFFFFFFF 0x0 0 175 | W 2 298.823713 2 0xf7000032 0xDFFFFFFF 0x0 0 176 | W 4 298.823715 2 0xf7000033 0xCFFFFFFF 0x0 0 177 | W 2 298.823718 2 0xf7000034 0xBFFFFFFF 0x0 0 178 | W 2 298.823724 2 0xf7000035 0xAFFFFFFF 0x0 0 179 | W 2 298.823726 2 0xf7000036 0x9FFFFFFF 0x0 0 180 | W 2 298.823732 2 0xf7000037 0x8FFFFFFF 0x0 0 181 | W 2 298.823734 2 0xf7000038 0x7FFFFFFF 0x0 0 182 | W 2 298.823740 2 0xf7000039 0x6FFFFFFF 0x0 0 183 | W 4 298.823742 2 0xf700003A 0x5FFFFFFF 0x0 0 184 | W 2 298.823748 2 0xf700003B 0x4FFFFFFF 0x0 0 185 | W 2 298.823750 2 0xf700003C 0x3FFFFFFF 0x0 0 186 | W 2 298.823756 2 0xf700003D 0x2FFFFFFF 0x0 0 187 | W 4 298.823758 2 0xf700003E 0x1FFFFFFF 0x0 0 188 | W 2 298.823764 2 0xf700003F 0x0FFFFFFF 0x0 0 189 | W 2 298.823766 2 0xf7000040 0x0EFFFFFF 0x0 0 190 | W 2 298.823772 2 0xf7000041 0x0DFFFFFF 0x0 0 191 | W 4 298.823774 2 0xf7000042 0x0CFFFFFF 0x0 0 192 | W 2 298.823780 2 0xf7000043 0x0BFFFFFF 0x0 0 193 | W 2 298.823782 2 0xf7000044 0x0AFFFFFF 0x0 0 194 | W 2 298.823788 2 0xf7000045 0x09FFFFFF 0x0 0 195 | W 4 298.823790 2 0xf7000046 0x08FFFFFF 0x0 0 196 | W 2 298.823796 2 0xf7000047 0x07FFFFFF 0x0 0 197 | W 2 298.823798 2 0xf7000048 0x06FFFFFF 0x0 0 198 | W 2 298.823705 2 0xf7000049 0x05FFFFFF 0x0 0 199 | W 2 298.823707 2 0xf700004A 0x04FFFFFF 0x0 0 200 | W 2 298.823713 2 0xf700004B 0x03FFFFFF 0x0 0 201 | W 4 298.823715 2 0xf700004C 0x02FFFFFF 0x0 0 202 | W 2 298.823718 2 0xf700004D 0x01FFFFFF 0x0 0 203 | W 2 298.823724 2 0xf700004E 0x00FFFFFF 0x0 0 204 | -------------------------------------------------------------------------------- /mmio/application/verilog/verilog_models.py: -------------------------------------------------------------------------------- 1 | """Verilog models for pre-generated Verilog code.""" 2 | 3 | from io import StringIO 4 | 5 | from pydantic import BaseModel, ConfigDict, Field, field_validator 6 | 7 | from mmio.core.logger import LoggerManager 8 | 9 | logger = LoggerManager.get_logger(__name__) 10 | 11 | 12 | class VerilogModelBase(BaseModel): 13 | """Base class for all Verilog models with bar number.""" 14 | 15 | model_config = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True) 16 | 17 | 18 | class VerilogGenerator(VerilogModelBase): 19 | """Enhanced base generator with dynamic content support.""" 20 | 21 | bar_number: int 22 | 23 | @classmethod 24 | def get_available_bars(cls) -> list[int]: 25 | """Get list of BARs available for this generator. 26 | 27 | Returns: 28 | List[int]: List of available BAR numbers 29 | 30 | """ 31 | return [] 32 | 33 | def get_verilog_content(self, operation: str | None = None, field_type: str | None = None) -> StringIO: 34 | """Get Verilog content from a StringIO field. 35 | 36 | This method provides a generic way to access StringIO content from fields 37 | following the pattern verilog_[operation]_[field_type]. 38 | 39 | Args: 40 | operation: Optional operation type ("read" or "write") 41 | field_type: Optional field type (e.g., "rom", "counter", "cases", etc.) 42 | If None, looks for a field containing "header" 43 | 44 | Returns: 45 | StringIO content from the matching field 46 | 47 | Raises: 48 | ValueError: If operation is provided but invalid or if field not found 49 | 50 | """ 51 | if operation and operation.lower() not in ["read", "write"]: 52 | raise ValueError("Operation must be 'read' or 'write' if provided") 53 | 54 | field_name = "verilog" 55 | if operation and field_type: 56 | field_name += f"_{operation.lower()}_{field_type}" 57 | elif not operation and not field_type: 58 | for name in self.model_fields: 59 | if "header" in name.lower(): 60 | field_name = name 61 | break 62 | if "rom_init" in name.lower(): 63 | field_name = name 64 | break 65 | if "state_machine" in name.lower(): 66 | field_name = name 67 | break 68 | if "reset_counter" in name.lower(): 69 | field_name = name 70 | break 71 | else: 72 | field_name += f"_{field_type}" 73 | 74 | logger.info(f"Looking for field: {field_name}") 75 | 76 | try: 77 | value = getattr(self, field_name) 78 | if isinstance(value, StringIO): 79 | logger.info(f"Found StringIO field {field_name} with content: {value.getvalue()}") 80 | result = StringIO() 81 | result.write(value.getvalue()) 82 | result.seek(0) 83 | return result 84 | else: 85 | logger.warning(f"Field {field_name} exists but is not StringIO: {type(value)}") 86 | except AttributeError: 87 | logger.warning(f"Field {field_name} not found") 88 | 89 | logger.warning(f"Returning empty StringIO for {field_name}") 90 | return StringIO() 91 | 92 | def set_bar_data( 93 | self, 94 | addresses: list[str], 95 | bit_widths: dict[str, int], 96 | read_values: list[str], 97 | write_values: list[str], 98 | defaults: tuple[str, str], 99 | ) -> None: 100 | """Set BAR-specific data for the generator. 101 | 102 | Args: 103 | addresses: List of addresses for this BAR 104 | bit_widths: Dictionary mapping addresses to their required bit widths 105 | read_values: List of read values for this BAR 106 | write_values: List of write values for this BAR 107 | defaults: Tuple of (read_default, write_default) values 108 | 109 | """ 110 | logger.info(f"Setting BAR {self.bar_number} data:") 111 | logger.info(f"Addresses: {addresses}") 112 | logger.info(f"Bit widths: {bit_widths}") 113 | logger.info(f"Read values: {read_values}") 114 | logger.info(f"Write values: {write_values}") 115 | logger.info(f"Defaults: {defaults}") 116 | 117 | self._addresses = addresses 118 | self._bit_widths = bit_widths 119 | self._read_values = read_values 120 | self._write_values = write_values 121 | self._defaults = defaults 122 | 123 | @property 124 | def addresses(self) -> list[str]: 125 | """Get addresses for current BAR.""" 126 | return getattr(self, "_addresses", []) 127 | 128 | @property 129 | def bit_widths(self) -> dict[str, int]: 130 | """Get bit widths for current BAR.""" 131 | return getattr(self, "_bit_widths", {}) 132 | 133 | @property 134 | def read_values(self) -> list[str]: 135 | """Get read values for current BAR.""" 136 | return getattr(self, "_read_values", []) 137 | 138 | @property 139 | def write_values(self) -> list[str]: 140 | """Get write values for current BAR.""" 141 | return getattr(self, "_write_values", []) 142 | 143 | @property 144 | def defaults(self) -> tuple[str, str]: 145 | """Get default values (read, write) for current BAR.""" 146 | return getattr(self, "_defaults", ("00000000", "00000000")) 147 | 148 | def get_unique_sorted_addresses(self, addresses: list[str]) -> list[str]: 149 | """Helper function to deduplicate and sort addresses.""" 150 | return sorted(set(addresses)) 151 | 152 | 153 | class VerilogStatic(VerilogGenerator): 154 | """Data class with pre-generated static code for read/write operations.""" 155 | 156 | verilog_header: StringIO = Field(default_factory=StringIO) 157 | verilog_state_machine_start: StringIO = Field(default_factory=StringIO) 158 | verilog_state_machine_end: StringIO = Field(default_factory=StringIO) 159 | 160 | @field_validator("verilog_header", "verilog_state_machine_start", "verilog_state_machine_end") 161 | @classmethod 162 | def validate_static(cls, value: StringIO) -> StringIO: 163 | """Validate static code format.""" 164 | return value 165 | 166 | 167 | class ROMEntry(VerilogGenerator): 168 | """Data class with pre-generated ROM entries for read/write operations.""" 169 | 170 | verilog_read_rom: StringIO = Field(default_factory=StringIO) 171 | verilog_write_rom: StringIO = Field(default_factory=StringIO) 172 | verilog_read_rom_init: StringIO = Field(default_factory=StringIO) 173 | verilog_write_rom_init: StringIO = Field(default_factory=StringIO) 174 | 175 | @field_validator("verilog_read_rom", "verilog_write_rom", "verilog_read_rom_init", "verilog_write_rom_init") 176 | @classmethod 177 | def validate_rom(cls, value: StringIO) -> StringIO: 178 | """Validate ROM format.""" 179 | return value 180 | 181 | def get_rom_name(self, address: str, operation: str) -> str: 182 | """Get ROM name for a specific address and operation.""" 183 | return f"{operation.upper()}_{address}" 184 | 185 | 186 | class AddressCheckEntry(VerilogGenerator): 187 | """Data class with pre-generated address checks for read/write operations.""" 188 | 189 | verilog_read_addr_check: StringIO = Field(default_factory=StringIO) 190 | verilog_write_addr_check: StringIO = Field(default_factory=StringIO) 191 | 192 | @field_validator("verilog_read_addr_check", "verilog_write_addr_check") 193 | @classmethod 194 | def validate_address(cls, value: StringIO) -> StringIO: 195 | """Validate address format.""" 196 | return value 197 | 198 | 199 | class CounterEntry(VerilogGenerator): 200 | """Data class with pre-generated counters for read/write operations.""" 201 | 202 | verilog_read_counter: StringIO = Field(default_factory=StringIO) 203 | verilog_write_counter: StringIO = Field(default_factory=StringIO) 204 | verilog_reset_read_counter: StringIO = Field(default_factory=StringIO) 205 | verilog_reset_write_counter: StringIO = Field(default_factory=StringIO) 206 | 207 | @field_validator( 208 | "verilog_read_counter", 209 | "verilog_write_counter", 210 | "verilog_reset_read_counter", 211 | "verilog_reset_write_counter", 212 | ) 213 | @classmethod 214 | def validate_counter(cls, value: StringIO) -> StringIO: 215 | """Validate counter format.""" 216 | return value 217 | 218 | def get_counter_name(self, address: str, operation: str) -> str: 219 | """Get counter name for a specific address and operation.""" 220 | return f"{operation.upper()}_C_{address}" 221 | 222 | 223 | class LogicEntries(VerilogGenerator): 224 | """Data class with pre-generated logic for read/write operations.""" 225 | 226 | verilog_read_cases: StringIO = Field(default_factory=StringIO) 227 | verilog_write_cases: StringIO = Field(default_factory=StringIO) 228 | 229 | @field_validator("verilog_read_cases", "verilog_write_cases") 230 | @classmethod 231 | def validate_cases(cls, value: StringIO) -> StringIO: 232 | """Validate cases format.""" 233 | return value 234 | -------------------------------------------------------------------------------- /mmio/application/cli/commands/mmio_select.py: -------------------------------------------------------------------------------- 1 | """MMIO select CLI.""" 2 | 3 | from collections.abc import Callable 4 | from pathlib import Path 5 | from typing import Any 6 | 7 | import click 8 | from click import Argument, Group, Option 9 | from click import Command as ClickCommand 10 | from pydantic import BaseModel, ConfigDict, Field 11 | from rich import box 12 | from rich.console import Console 13 | from rich.panel import Panel 14 | from rich.table import Table 15 | from rich.text import Text 16 | 17 | from mmio.core.logger import LoggerManager 18 | from mmio.infrastructure.cli.base import CLIBase, CLICommand 19 | 20 | logger = LoggerManager.get_logger(__name__) 21 | console = Console() 22 | 23 | 24 | class SelectMMIOFileInputCLI(BaseModel): 25 | """Select MMIO file input CLI. 26 | 27 | This class provides the CLI interface for selecting and managing MMIO input files. 28 | It handles file listing, selection, and validation through user interaction. 29 | 30 | Available options: 31 | --retrieve-list: Retrieve all available MMIO files 32 | --select-file: Select an MMIO file by name 33 | --validate-file: Validate the currently selected file 34 | """ 35 | 36 | model_config = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True) 37 | 38 | cli_base: CLIBase = Field(default_factory=CLIBase) 39 | cli_commands: CLICommand = Field(default_factory=lambda: CLICommand(name="mmio")) 40 | selected_file: Path | None = None 41 | _list_files_callback: Callable[[], list[Path]] | None = None 42 | 43 | def model_post_init(self, __context: Any) -> None: 44 | """Initialize after model creation.""" 45 | self.cli_commands.group = self.mmio_group 46 | self.cli_commands.help = "Select and manage MMIO input files" 47 | self.cli_commands.epilog = "Use this command to select MMIO log files for processing" 48 | self.cli_commands.short_help = "MMIO file selection" 49 | self.cli_commands.options_metavar = "[OPTIONS]" 50 | self.cli_commands.add_help_option = True 51 | self.cli_commands.no_args_is_help = False 52 | self.cli_commands.hidden = False 53 | self.cli_commands.deprecated = False 54 | self._setup_default_options() 55 | 56 | @property 57 | def mmio_group(self) -> Group: 58 | """Get the mmio group.""" 59 | return Group(name="mmio") 60 | 61 | @property 62 | def mmio_name(self) -> str: 63 | """Get the mmio name.""" 64 | return "mmio" 65 | 66 | def set_list_files_callback(self, callback: Callable[[], list[Path]]) -> None: 67 | """Set the callback for listing files. 68 | 69 | Args: 70 | callback: Function that returns a list of available files 71 | 72 | """ 73 | self._list_files_callback = callback 74 | 75 | def _setup_default_options(self) -> None: 76 | """Set up default CLI options.""" 77 | self.cli_commands.params = [ 78 | click.Option( 79 | ["--retrieve-list"], 80 | is_flag=True, 81 | help="Retrieve all available MMIO files", 82 | callback=self._list_files_option_callback, 83 | ), 84 | click.Option( 85 | param_decls=["--select-file"], 86 | type=str, 87 | help="Select an MMIO file by name", 88 | callback=self._select_file_callback, 89 | ), 90 | click.Option( 91 | param_decls=["--validate-file"], 92 | is_flag=True, 93 | help="Validate the currently selected file", 94 | callback=self._validate_file_callback, 95 | ), 96 | ] 97 | 98 | def add_command(self, command: ClickCommand) -> None: 99 | """Add a command to the CLI.""" 100 | self.cli_base.add_group(group=self.mmio_group) 101 | self.cli_base.add_command(command=command) 102 | 103 | def add_argument(self, *args: Any, **kwargs: Any) -> None: 104 | """Add an argument to the CLI.""" 105 | self.cli_commands.params.append(Argument(*args, **kwargs)) 106 | 107 | def add_option(self, *args: Any, **kwargs: Any) -> None: 108 | """Add an option to the CLI.""" 109 | option = Option(*args, **kwargs) 110 | self.cli_commands.add_parameter(option) 111 | 112 | def add_help_option(self) -> None: 113 | """Add a help option to the CLI.""" 114 | self.cli_commands.add_help_option = True 115 | 116 | def add_group(self, group: Group) -> None: 117 | """Add a group to the CLI.""" 118 | self.cli_base.add_group(group=group) 119 | 120 | def setup_commands(self) -> None: 121 | """Set up all commands for the CLI.""" 122 | self.cli_base.setup_commands() 123 | 124 | def prompt_user(self) -> Path | None: 125 | """Prompt the user to select an MMIO file.""" 126 | logger.info("Starting interactive file selection") 127 | 128 | title = Text("MMIO File Selection", style="bold blue") 129 | title.align("center", width=60) 130 | 131 | files = self._list_files() 132 | logger.info(f"Found {len(files)} available files") 133 | 134 | if not files: 135 | logger.warning("No MMIO files available") 136 | console.print( 137 | Panel( 138 | "[yellow]No MMIO files available in the current directory.[/yellow]", 139 | title="File Selection", 140 | title_align="center", 141 | box=box.ROUNDED, 142 | width=60, 143 | ) 144 | ) 145 | return None 146 | 147 | table = Table( 148 | show_header=True, 149 | header_style="bold cyan", 150 | box=box.SIMPLE, 151 | title="Available Files", 152 | title_style="bold blue", 153 | width=56, 154 | ) 155 | table.add_column("#", style="dim", width=4) 156 | table.add_column("File Name", style="green") 157 | 158 | for idx, file in enumerate(files, 1): 159 | logger.info(f"File option {idx}: {file}") 160 | table.add_row(str(idx), file.name) 161 | 162 | console.print( 163 | Panel(title, box=box.DOUBLE, padding=(1, 2), title="File Selection", title_align="center", width=60) 164 | ) 165 | 166 | console.print(Panel(table, box=box.ROUNDED, padding=(1, 1), width=60)) 167 | 168 | try: 169 | selection = click.prompt("\nSelect a file (enter number or full name)", type=str, default="1") 170 | logger.info(f"User entered selection: {selection}") 171 | 172 | if selection.isdigit(): 173 | idx = int(selection) - 1 174 | logger.info(f"Numeric selection: index {idx}") 175 | if 0 <= idx < len(files): 176 | selected_file = files[idx] 177 | logger.info(f"Selected file by index: {selected_file}") 178 | else: 179 | logger.error(f"Invalid selection number: {selection}") 180 | console.print("[red]❌ Invalid selection number[/red]") 181 | return None 182 | else: 183 | logger.info(f"Name selection: {selection}") 184 | matching_files = [f for f in files if f.name == selection] 185 | if matching_files: 186 | selected_file = matching_files[0] 187 | logger.info(f"Selected file by name: {selected_file}") 188 | else: 189 | logger.error(f"File not found: {selection}") 190 | console.print("[red]❌ File not found[/red]") 191 | return None 192 | 193 | self.selected_file = selected_file 194 | logger.info(f"Successfully set selected file: {self.selected_file}") 195 | console.print(f"\n[green]✓ Selected:[/green] {selected_file.name}") 196 | return selected_file 197 | 198 | except click.Abort: 199 | logger.warning("User cancelled selection") 200 | console.print("\n[yellow]Selection cancelled[/yellow]") 201 | return None 202 | except Exception as e: 203 | logger.error(f"Error during selection: {e}", exc_info=True) 204 | console.print(f"\n[red]Error during selection: {e}[/red]") 205 | return None 206 | 207 | def _list_files(self) -> list[Path]: 208 | """List available MMIO files.""" 209 | logger.info("Attempting to list files") 210 | if self._list_files_callback: 211 | logger.info("Using list_files_callback") 212 | files = self._list_files_callback() 213 | logger.info(f"Found {len(files)} files using callback") 214 | return files 215 | logger.warning("No list_files_callback set") 216 | return [] 217 | 218 | def _list_files_option_callback(self, ctx: click.Context, param: click.Parameter, value: bool) -> None: 219 | """Callback for --retrieve-list option.""" 220 | if not value: 221 | return 222 | files = self._list_files() 223 | if files: 224 | click.echo("\nAvailable MMIO files:") 225 | for file in files: 226 | click.echo(f"- {file.name}") 227 | else: 228 | click.echo("No MMIO files available") 229 | 230 | def _select_file_callback(self, ctx: click.Context, param: click.Parameter, value: str) -> None: 231 | """Callback for --select option.""" 232 | if not value: 233 | return 234 | files = self._list_files() 235 | matching_files = [f for f in files if f.name == value] 236 | if matching_files: 237 | self.selected_file = matching_files[0] 238 | click.echo(f"Selected file: {self.selected_file.name}") 239 | else: 240 | click.echo(f"File not found: {value}") 241 | 242 | def _validate_file_callback(self, ctx: click.Context, param: click.Parameter, value: bool) -> None: 243 | """Callback for --validate option.""" 244 | if not value: 245 | return 246 | if not self.selected_file: 247 | click.echo("No file selected to validate") 248 | return 249 | click.echo(f"Validating file: {self.selected_file.name}") 250 | -------------------------------------------------------------------------------- /mmio/domain/models/verilog_data.py: -------------------------------------------------------------------------------- 1 | from typing import Any, ClassVar 2 | 3 | from pydantic import BaseModel, ConfigDict, field_validator 4 | 5 | from mmio.core.exceptions import ValidationError 6 | from mmio.core.logger import LoggerManager 7 | 8 | logger = LoggerManager.get_logger(__name__) 9 | 10 | 11 | class _MMIOTracker: 12 | """Internal tracker for MMIO data and statistics.""" 13 | 14 | all_instances: list["VerilogData"] = [] 15 | address_bit_widths: dict[str, int] = {} 16 | default_values: dict[int, tuple[str, str]] = {} 17 | 18 | @classmethod 19 | def add_instance(cls, instance: "VerilogData") -> None: 20 | """Add a VerilogData instance to tracking.""" 21 | cls.all_instances.append(instance) 22 | 23 | @classmethod 24 | def update_address_bit_width(cls, address: str | None, value: str | None) -> None: 25 | """Update the maximum bit width needed for a given address.""" 26 | if address is None or value is None: 27 | return 28 | 29 | current_width = VerilogData.calculate_bit_width(value) 30 | existing_width = cls.address_bit_widths.get(address, 0) 31 | cls.address_bit_widths[address] = max(current_width, existing_width) 32 | 33 | @classmethod 34 | def update_default_values(cls, bar: int | None, operation: str | None, value: str | None) -> None: 35 | """Update default values for a BAR based on new value.""" 36 | if bar is None or operation is None or value is None: 37 | return 38 | 39 | if bar not in cls.default_values: 40 | cls.default_values[bar] = ("00000000", "00000000") 41 | 42 | current_defaults = cls.default_values[bar] 43 | if operation == "R": 44 | cls.default_values[bar] = (value, current_defaults[1]) 45 | else: 46 | cls.default_values[bar] = (current_defaults[0], value) 47 | 48 | @classmethod 49 | def get_default_values(cls, bar: int) -> tuple[str, str]: 50 | """Get default read/write values for a BAR.""" 51 | return cls.default_values.get(bar, ("00000000", "00000000")) 52 | 53 | @classmethod 54 | def get_bar_address_bit_widths(cls, bar: int) -> dict[str, int]: 55 | """Get all addresses and their bit widths for a specific BAR.""" 56 | bar_addresses = set(VerilogData.addresses(bar=bar)) 57 | return {addr: width for addr, width in cls.address_bit_widths.items() if addr in bar_addresses} 58 | 59 | 60 | class VerilogData(BaseModel): 61 | """Data class for MMIO data.""" 62 | 63 | model_config = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True) 64 | 65 | _tracker: ClassVar[_MMIOTracker] = _MMIOTracker() 66 | 67 | operation: str | None = None 68 | bar: int | None = None 69 | value: str | None = None 70 | address: str | None = None 71 | register_value: str | None = None 72 | timestamp: float | None = None 73 | description: str | None = None 74 | 75 | @classmethod 76 | def from_dict(cls, data_dict: dict[str, Any]) -> "VerilogData": 77 | """Create a VerilogData instance from a dictionary and update class tracking. 78 | 79 | Args: 80 | data_dict: Dictionary containing the MMIO data fields 81 | 82 | Returns: 83 | VerilogData instance with all class tracking updated 84 | 85 | Raises: 86 | ValidationError: If the data is invalid 87 | 88 | """ 89 | try: 90 | if "bar" in data_dict and data_dict["bar"] is not None: 91 | data_dict["bar"] = int(data_dict["bar"]) 92 | if "timestamp" in data_dict and data_dict["timestamp"] is not None: 93 | data_dict["timestamp"] = float(data_dict["timestamp"]) 94 | 95 | for field in ["address", "register_value", "value"]: 96 | if field in data_dict and data_dict[field] is not None: 97 | data_dict[field] = str(data_dict[field]) 98 | 99 | instance = cls( 100 | operation=data_dict.get("operation"), 101 | bar=data_dict.get("bar"), 102 | value=data_dict.get("value"), 103 | address=data_dict.get("address"), 104 | register_value=data_dict.get("register_value"), 105 | timestamp=data_dict.get("timestamp"), 106 | description=data_dict.get("description"), 107 | ) 108 | 109 | instance.format_and_validate() 110 | 111 | cls._tracker.add_instance(instance) 112 | 113 | if instance.address is not None and instance.value is not None: 114 | cls._tracker.update_address_bit_width(instance.address, instance.value) 115 | 116 | if instance.bar is not None and instance.operation is not None and instance.value is not None: 117 | cls._tracker.update_default_values(instance.bar, instance.operation, instance.value) 118 | 119 | return instance 120 | except Exception as e: 121 | logger.error(f"Error creating VerilogData from dict: {e}") 122 | raise ValidationError(f"Failed to create VerilogData: {str(e)}") 123 | 124 | def format_and_validate(self) -> None: 125 | """Format all values and validate them.""" 126 | if self.value is not None: 127 | self.value = self.remove_0x(self.value, self.address) 128 | if self.register_value is not None: 129 | self.register_value = self.remove_0x(self.register_value, self.address) 130 | if self.address is not None: 131 | self.address = self.remove_0x(self.address, None) 132 | 133 | if self.address is not None: 134 | self.address = self.format_value(self.address, is_address=True) 135 | if self.value is not None: 136 | self.value = self.format_value(self.value, is_address=False) 137 | if self.register_value is not None: 138 | self.register_value = self.format_value(self.register_value, is_address=False) 139 | if self.timestamp is not None: 140 | self.timestamp = self.normalize_timestamp(self.timestamp) 141 | 142 | if self.bar is not None: 143 | self.bar = self.validate_bar(self.bar) 144 | if self.operation is not None: 145 | self.operation = self.validate_operation(self.operation) 146 | if self.address is not None: 147 | self.address = self.validate_address(self.address) 148 | if self.register_value is not None: 149 | self.register_value = self.validate_register_value(self.register_value) 150 | self.timestamp = self.validate_timestamp(self.timestamp) 151 | 152 | @classmethod 153 | def get_all_instances(cls) -> list["VerilogData"]: 154 | """Get all VerilogData instances.""" 155 | return cls._tracker.all_instances 156 | 157 | @classmethod 158 | def get_address_bit_widths(cls) -> dict[str, int]: 159 | """Get all address bit widths.""" 160 | return cls._tracker.address_bit_widths 161 | 162 | @classmethod 163 | def get_default_values(cls, bar: int) -> tuple[str, str]: 164 | """Get default read/write values for a BAR.""" 165 | return cls._tracker.get_default_values(bar) 166 | 167 | @classmethod 168 | def get_bar_address_bit_widths(cls, bar: int) -> dict[str, int]: 169 | """Get all addresses and their bit widths for a specific BAR.""" 170 | return cls._tracker.get_bar_address_bit_widths(bar) 171 | 172 | @field_validator("operation") 173 | @classmethod 174 | def validate_operation(cls, value: str | None) -> str | None: 175 | """Validate the operation.""" 176 | if value is None: 177 | return None 178 | if value not in ["R", "W"]: 179 | raise ValidationError(f"Invalid operation: {value}") 180 | return value 181 | 182 | @field_validator("address") 183 | @classmethod 184 | def validate_address(cls, value: str | None) -> str | None: 185 | """Validate the address.""" 186 | if value is None: 187 | return None 188 | try: 189 | int(value, 16) 190 | return value[-5:] 191 | except ValueError: 192 | raise ValidationError(f"Invalid address format: {value}") 193 | 194 | @field_validator("bar") 195 | @classmethod 196 | def validate_bar(cls, value: int | None) -> int | None: 197 | """Validate the bar.""" 198 | if value is None: 199 | return None 200 | if value == 0: 201 | return 0 202 | if not 0 <= value <= 9: 203 | raise ValidationError(f"BAR number out of range: {value}") 204 | return value 205 | 206 | @field_validator("register_value") 207 | @classmethod 208 | def validate_register_value(cls, value: str | None) -> str | None: 209 | """Validate the register value.""" 210 | if value is None: 211 | return None 212 | if value == "0": 213 | return "00000000" 214 | try: 215 | int(value, 16) 216 | return value[-8:] 217 | except ValueError as e: 218 | raise ValidationError(f"Invalid register value format: {value} - {e}") 219 | 220 | @field_validator("timestamp") 221 | @classmethod 222 | def validate_timestamp(cls, value: float | None) -> float | None: 223 | """Validate timestamp value.""" 224 | if value is None: 225 | return None 226 | if value < 0: 227 | raise ValidationError("Timestamp cannot be negative") 228 | return value 229 | 230 | @classmethod 231 | def normalize_timestamp(cls, timestamp: float) -> int: 232 | """Strip the timestamp from a value and remove the first two integers.""" 233 | seconds: str = str(timestamp)[-1] 234 | nanos: str = str(timestamp).split(".")[1].ljust(6, "0") 235 | values: int = int(f"{seconds}{nanos}") 236 | logger.info(f"Normalized timestamp: {values}") 237 | return values 238 | 239 | @classmethod 240 | def remove_0x(cls, value: str | None, address: str | None) -> str | None: 241 | """Remove the 0x prefix from a value.""" 242 | if value is None and address is None: 243 | return None 244 | if value is not None: 245 | return value.replace("0x", "") 246 | if address is not None: 247 | return address.replace("0x", "") 248 | return value 249 | 250 | @classmethod 251 | def format_value(cls, value: str | None, is_address: bool = False) -> str | None: 252 | """Format hex values to proper width.""" 253 | if value is None: 254 | return None 255 | try: 256 | num = int(value, 16) 257 | return f"{num:05X}" if is_address else f"{num:08X}" 258 | except ValueError: 259 | return None 260 | 261 | @staticmethod 262 | def calculate_bit_width(value: str | None) -> int: 263 | """Calculate required bit width for a value.""" 264 | if not value: 265 | return 32 266 | try: 267 | num = int(value, 16) 268 | return max(num.bit_length(), 1) 269 | except ValueError: 270 | return 32 271 | 272 | @classmethod 273 | def read_values(cls, bar: int | None = None) -> list[str]: 274 | """Get all read values, optionally filtered by BAR.""" 275 | return [ 276 | data.value 277 | for data in cls._tracker.all_instances 278 | if data.operation == "R" and data.value is not None and (bar is None or data.bar == bar) 279 | ] 280 | 281 | @classmethod 282 | def write_values(cls, bar: int | None = None) -> list[str]: 283 | """Get all write values, optionally filtered by BAR.""" 284 | return [ 285 | data.value 286 | for data in cls._tracker.all_instances 287 | if data.operation == "W" and data.value is not None and (bar is None or data.bar == bar) 288 | ] 289 | 290 | @classmethod 291 | def addresses(cls, bar: int | None = None) -> list[str]: 292 | """Get all addresses, optionally filtered by BAR.""" 293 | return [ 294 | data.address 295 | for data in cls._tracker.all_instances 296 | if data.address is not None and (bar is None or data.bar == bar) 297 | ] 298 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MMIO to Verilog Generator 2 | 3 | This package generates Verilog code from MMIO logs. 4 | 5 | ## Usage 6 | 7 | ### Run the script 8 | 9 | - run.bat will install the dependencies and create a .venv folder, and then run the script after activating the venv. 10 | --- 11 | ![image](https://github.com/user-attachments/assets/17835f01-580a-41eb-8549-8776321082dd) 12 | 13 | --- 14 | ![image](https://github.com/user-attachments/assets/fcbe3489-416b-4a09-aeed-8cecba06e45a) 15 | 16 | --- 17 | ![image](https://github.com/user-attachments/assets/5ea2aa7b-a502-49ea-906e-0b13eac15c7f) 18 | 19 | --- 20 | ### Adding your own generator and Verilog block 21 | 22 | - Make a new data class in `mmio/application/verilog/verilog_models.py` if you want to add a new type of Verilog "Entry class" (Your own read/write response logic, ROM, counter, etc). 23 | 24 | ```py 25 | """mmio/application/verilog/verilog_models.py""" 26 | 27 | # Might have to add your field types to the if statement below (depends on what you named your fields) 28 | def get_verilog_content(self, operation: str | None = None, field_type: str | None = None) -> StringIO: 29 | """Get Verilog content from a StringIO field.""" 30 | if operation and operation.lower() not in ["read", "write"]: 31 | raise ValueError("Operation must be 'read' or 'write' if provided") 32 | 33 | field_name = "verilog" 34 | if operation and field_type: 35 | field_name += f"_{operation.lower()}_{field_type}" 36 | elif not operation and not field_type: 37 | for name in self.model_fields: 38 | if "header" in name.lower(): 39 | field_name = name 40 | break 41 | if "rom_init" in name.lower(): 42 | field_name = name 43 | break 44 | if "state_machine" in name.lower(): 45 | field_name = name 46 | break 47 | if "reset_counter" in name.lower(): 48 | field_name = name 49 | break 50 | 51 | ... 52 | ... 53 | 54 | # Make a new data class in `mmio/application/verilog/verilog_models.py`: 55 | class YourNewEntry(VerilogGenerator): 56 | """Data class with pre-generated ROM entries for read/write operations.""" 57 | 58 | verilog_read_rom: StringIO = Field(default_factory=StringIO) 59 | verilog_write_rom: StringIO = Field(default_factory=StringIO) 60 | verilog_read_rom_init: StringIO = Field(default_factory=StringIO) 61 | verilog_write_rom_init: StringIO = Field(default_factory=StringIO) 62 | 63 | @field_validator("verilog_read_rom", "verilog_write_rom", "verilog_read_rom_init", "verilog_write_rom_init") 64 | @classmethod 65 | def validate_rom(cls, value: StringIO) -> StringIO: 66 | """Validate ROM format.""" 67 | return value 68 | 69 | def get_rom_name(self, address: str, operation: str) -> str: 70 | """Get ROM name for a specific address and operation.""" 71 | return f"{operation.upper()}_{address}" 72 | ``` 73 | 74 | --- 75 | 76 | ## Building the generated Verilog code 77 | 78 | - Make a new file in `mmio/application/verilog/generators/` and a new class if you want to add a new type of Verilog block (ROM, counter, address check, logic). Look at the other files in the folder for examples. 79 | 80 | - Existing generators: 81 | 82 | ```txt 83 | mmio/application/verilog/generators/address_check.py 84 | mmio/application/verilog/generators/counter_generator.py 85 | mmio/application/verilog/generators/response_logic.py 86 | mmio/application/verilog/generators/rom.py 87 | mmio/application/verilog/generators/static_generator.py 88 | ``` 89 | 90 | - The class `StaticCodeGenerator` is already implemented, and can be used as a template for new generators. 91 | 92 | ```py 93 | class StaticCodeGenerator(VerilogGenerator): 94 | """Verilog module static code generator.""" 95 | 96 | static_code: VerilogStatic = Field( 97 | default_factory=lambda: VerilogStatic( 98 | bar_number=0, 99 | verilog_header=StringIO(), 100 | verilog_state_machine_start=StringIO(), 101 | verilog_state_machine_end=StringIO(), 102 | ) 103 | ) 104 | 105 | verilog_settings: VerilogSettings = Field(default_factory=lambda: VerilogSettings.get_instance()) 106 | save_header: StringIO = Field(default_factory=StringIO) 107 | ``` 108 | 109 | ```py 110 | """Example of a simple start of a state machine.""" 111 | 112 | # Your StringIO object is built here, and is saved to the data class: 113 | def generate_state_machine_start(self) -> VerilogStatic: 114 | """Generate the Verilog state machine with all ports.""" 115 | self.save_state_machine.seek(0) 116 | self.save_state_machine.truncate() 117 | 118 | self.save_state_machine.write(" always_ff @(posedge clk) begin\n") 119 | self.save_state_machine.write(" if (rst) begin\n") 120 | self.save_state_machine.write(" rd_rsp_valid <= 1'b0;\n") 121 | 122 | self.static_code.verilog_state_machine_start = StringIO(self.save_state_machine.getvalue()) 123 | return self.static_code 124 | ``` 125 | 126 | --- 127 | 128 | - Add your new class and generator to the `mmio/application/verilog/verilog_builder_orchestrator.py` file. 129 | 130 | ```py 131 | """mmio/application/verilog/verilog_builder_orchestrator.py""" 132 | 133 | class GeneratorType(Enum): 134 | """Enum for generator types.""" 135 | 136 | # Add your new generator type here: 137 | ROM = "rom" 138 | COUNTER = "counter" 139 | ADDRESS_CHECK = "addr_check" 140 | HEADER = "header" 141 | STATE_MACHINE_START = "state_machine_start" 142 | STATE_MACHINE_END = "state_machine_end" 143 | LOGIC = "logic" 144 | RESET_COUNTER = "reset_counter" 145 | ROM_INIT = "rom_init" 146 | 147 | # Initialize your new StringIO objects here: 148 | verilog_rom: ROMEntry = Field( 149 | default_factory=lambda: ROMEntry( 150 | bar_number=0, 151 | verilog_read_rom=StringIO(), 152 | verilog_write_rom=StringIO(), 153 | verilog_read_rom_init=StringIO(), 154 | verilog_write_rom_init=StringIO(), 155 | ) 156 | ) 157 | 158 | # Add your new generator initialization here: 159 | def __init__(self, **data: Any): 160 | """Initialize the VerilogBuilderOrchestrator with registered generators.""" 161 | super().__init__(**data) 162 | logger.info("Initializing VerilogBuilderOrchestrator") 163 | 164 | # Register default generators 165 | self.register_generator(GeneratorType.ROM.value, ROMGenerator) 166 | self.register_generator(GeneratorType.ADDRESS_CHECK.value, AddressCheckGenerator) 167 | self.register_generator(GeneratorType.COUNTER.value, CounterGenerator) 168 | self.register_generator(GeneratorType.RESET_COUNTER.value, CounterGenerator) 169 | self.register_generator(GeneratorType.HEADER.value, StaticCodeGenerator) 170 | self.register_generator(GeneratorType.STATE_MACHINE_START.value, StaticCodeGenerator) 171 | self.register_generator(GeneratorType.STATE_MACHINE_END.value, StaticCodeGenerator) 172 | self.register_generator(GeneratorType.LOGIC.value, ResponseLogicGenerator) 173 | self.register_generator(GeneratorType.ROM_INIT.value, ROMGenerator) 174 | 175 | ... 176 | ... 177 | 178 | # Add your new generator function here: 179 | def _handle_rom(self, generator: ROMGenerator, operation: str | None) -> str: 180 | """Handle ROM field type generation.""" 181 | generator.generate_rom_structure(self.bar_number) 182 | self.verilog_rom = generator.verilog_rom 183 | return self._get_operation_value( 184 | operation, self.verilog_rom.verilog_read_rom, self.verilog_rom.verilog_write_rom 185 | ) 186 | 187 | ... 188 | ... 189 | 190 | # Add your new generator function to the match case: 191 | try: 192 | generator = self._get_generator_instance(field_type, bar_number) 193 | 194 | match field_type: 195 | case "header" if isinstance(generator, StaticCodeGenerator): 196 | return self._handle_header(generator) 197 | 198 | case "state_machine_start" if isinstance(generator, StaticCodeGenerator): 199 | return self._handle_state_machine_start(generator) 200 | 201 | case "state_machine_end" if isinstance(generator, StaticCodeGenerator): 202 | return self._handle_state_machine_end(generator) 203 | 204 | case "rom" if isinstance(generator, ROMGenerator): 205 | return self._handle_rom(generator, operation) 206 | 207 | case "rom_init" if isinstance(generator, ROMGenerator): 208 | return self._handle_rom_init(generator, operation) 209 | 210 | case "counter" if isinstance(generator, CounterGenerator): 211 | return self._handle_counter(generator, operation) 212 | 213 | case "reset_counter" if isinstance(generator, CounterGenerator): 214 | return self._handle_reset_counter(generator, operation) 215 | 216 | case "addr_check" if isinstance(generator, AddressCheckGenerator): 217 | return self._handle_addr_check(generator, operation) 218 | 219 | case "logic" if isinstance(generator, ResponseLogicGenerator): 220 | return self._handle_logic(generator, operation) 221 | 222 | case _: 223 | logger.warning(f"No handler found for field type: {field_type}") 224 | return "" 225 | ``` 226 | 227 | --- 228 | 229 | **Add a new function and build the `mmio/application/cli/coordinator/modular_orchestrator.py` file.** 230 | 231 | ```py 232 | """mmio/application/verilog/modular_orchestrator.py""" 233 | 234 | def _generate_rom_structures( 235 | self, 236 | bar_number: Any, 237 | generate_read: bool, 238 | generate_write: bool, 239 | ) -> list[str]: 240 | """Generate ROM structures for a BAR.""" 241 | lines: list[str] = [] 242 | logger.info("Generating ROM structure") 243 | if generate_read: 244 | rom_code = self.verilog_orch.build_verilog(operation="read", bar_number=bar_number, field_type="rom") 245 | self._append_code_block(lines, rom_code, bar_number, "ROM", "Read") 246 | if generate_write: 247 | rom_code = self.verilog_orch.build_verilog(operation="write", bar_number=bar_number, field_type="rom") 248 | self._append_code_block(lines, rom_code, bar_number, "ROM", "Write") 249 | return lines 250 | 251 | ... 252 | ... 253 | 254 | # Add your function to the bar structure generation (what/where does it need to be?): 255 | def _generate_bar_structures( 256 | self, 257 | bar_numbers: list[Any], 258 | processed_data: dict[Any, Any], 259 | cli_options: CLIOptions, 260 | generate_read: bool, 261 | generate_write: bool, 262 | ) -> list[str]: 263 | """Generate all structures for each BAR.""" 264 | lines: list[str] = [] 265 | for bar_number in bar_numbers: 266 | if bar_number not in processed_data: 267 | logger.warning(f"BAR {bar_number} not found in processed data, skipping") 268 | continue 269 | 270 | logger.info(f"Generating structures for BAR {bar_number}") 271 | 272 | lines.extend( 273 | self._generate_rom_structures( 274 | bar_number, 275 | generate_read, 276 | generate_write, 277 | ) 278 | ) 279 | 280 | if cli_options.include_counters: 281 | lines.extend( 282 | self._generate_counter_structures( 283 | bar_number, 284 | generate_read, 285 | generate_write, 286 | ) 287 | ) 288 | 289 | if cli_options.include_address_checks: 290 | lines.extend( 291 | self._generate_address_check_structures( 292 | bar_number, 293 | generate_read, 294 | generate_write, 295 | ) 296 | ) 297 | ``` 298 | 299 | --- 300 | 301 | ## Add the new object to the CLI Base class 302 | 303 | **Add the new object to the variable initialization:** 304 | 305 | ```py 306 | """mmio/config/cli_config.py""" 307 | 308 | # Add shit here: 309 | class CLIOptions(BaseModel): 310 | """CLI Options for controlling the Verilog generation process.""" 311 | 312 | model_config = ConfigDict(arbitrary_types_allowed=True) 313 | 314 | bar_selection: list[int] | None = Field(default=None, description="List of BARs to process") 315 | operation_filter: str = Field(default="B", description="Operation filter (R/W/B)") 316 | include_address_checks: bool = Field(default=True, description="Whether to include address check structures") 317 | include_counters: bool = Field(default=True, description="Whether to include counter structures") 318 | include_default_values: bool = Field(default=True, description="Whether to include default value structures") 319 | include_logic: bool = Field(default=True, description="Whether to include logic structures") 320 | include_state_machines: bool = Field(default=True, description="Whether to include state machine structures") 321 | init_roms: bool = Field(default=True, description="Whether to include ROM initialization structures").py""" 322 | ``` 323 | 324 | --- 325 | 326 | ## I don't want to write anymore, you can figure out the rest yourself :D 327 | -------------------------------------------------------------------------------- /mmio/application/cli/app.py: -------------------------------------------------------------------------------- 1 | """Application logic for the CLI interface.""" 2 | 3 | from pathlib import Path 4 | from typing import Any 5 | 6 | import click 7 | from pydantic import BaseModel, Field 8 | from rich import box 9 | from rich.console import Console 10 | from rich.panel import Panel 11 | from rich.text import Text 12 | 13 | from mmio.application.cli.commands.file_select import BaseFileSelectCLI 14 | from mmio.application.cli.commands.log_settings import LogSettingsCLI 15 | from mmio.application.cli.commands.mmio_select import SelectMMIOFileInputCLI 16 | from mmio.application.cli.commands.output_select import OutputVerilogCLI 17 | from mmio.application.cli.coordinator.cli_coordinator import CLICoordinator 18 | from mmio.application.cli.coordinator.modular_orchestrator import ModularOrchestrator 19 | from mmio.application.cli.mmio import MMIOCLIManager 20 | from mmio.config.cli_config import CLIOptions 21 | from mmio.config.log_settings import LogSettings 22 | from mmio.config.mmio_settings import MMIOSettings 23 | from mmio.config.verilog_settings import VerilogSettings 24 | from mmio.core.logger import LoggerManager 25 | from mmio.infrastructure.file_handling.verilog_filemanager import VerilogFileManager 26 | 27 | logger = LoggerManager.get_logger(__name__) 28 | console = Console() 29 | 30 | 31 | class AppLogic(BaseModel): 32 | """Application logic for coordinating CLI components and orchestration. 33 | 34 | This class serves as the main coordinator between the CLI interface and the 35 | domain logic. It manages: 36 | 1. CLI components for file selection and configuration 37 | 2. Settings management 38 | 3. Orchestration of the main application flow 39 | """ 40 | 41 | file_select_cli: BaseFileSelectCLI = Field(default_factory=BaseFileSelectCLI) 42 | mmio_select_cli: SelectMMIOFileInputCLI = Field(default_factory=SelectMMIOFileInputCLI) 43 | output_select_cli: OutputVerilogCLI = Field(default_factory=OutputVerilogCLI) 44 | log_settings_cli: LogSettingsCLI = Field(default_factory=LogSettingsCLI) 45 | verilog_file_manager: VerilogFileManager = Field(default_factory=VerilogFileManager) 46 | 47 | mmio_cli_manager: MMIOCLIManager = Field(default_factory=MMIOCLIManager) 48 | 49 | mmio_settings: MMIOSettings = Field( 50 | default_factory=lambda: MMIOSettings.get_instance(), description="MMIO settings singleton instance" 51 | ) 52 | verilog_settings: VerilogSettings = Field( 53 | default_factory=lambda: VerilogSettings.get_instance(), description="Verilog settings singleton instance" 54 | ) 55 | log_instance: LogSettings = Field( 56 | default_factory=lambda: LogSettings.get_instance(), description="Log settings singleton instance" 57 | ) 58 | log_settings: LogSettings = Field( 59 | default_factory=lambda: LogSettings.load(), description="Loaded log settings from file" 60 | ) 61 | file_name: str = Field(default_factory=VerilogFileManager.get_instance().generate_output_filename) 62 | 63 | settings_path: Path | None = Field( 64 | default=None, description="Optional path to override mmio_settings.file_input_path" 65 | ) 66 | settings_output_path: Path | None = Field( 67 | default=None, description="Optional path to override verilog_settings.file_output_path" 68 | ) 69 | settings_module_header: str | None = Field( 70 | default=None, description="Optional value to override verilog_settings.module_header" 71 | ) 72 | 73 | orchestrator: ModularOrchestrator = Field(default_factory=ModularOrchestrator) 74 | 75 | def model_post_init(self, __context: Any) -> None: 76 | """Initialize settings after model creation. 77 | 78 | This method is called automatically by Pydantic after the model is created. 79 | It applies any overrides to the settings singletons before they are used. 80 | """ 81 | mmio = MMIOSettings.get_instance() 82 | verilog = VerilogSettings.get_instance() 83 | 84 | if self.settings_path is not None: 85 | mmio.file_input_path = self.settings_path 86 | logger.info(f"Overriding MMIO input path with: {self.settings_path}") 87 | 88 | if self.settings_output_path is not None: 89 | verilog.file_output_path = self.settings_output_path 90 | logger.info(f"Overriding Verilog output path with: {self.settings_output_path}") 91 | 92 | if self.settings_module_header is not None: 93 | verilog.module_header = self.settings_module_header 94 | logger.info(f"Overriding Verilog module header with: {self.settings_module_header}") 95 | 96 | def initialize(self) -> None: 97 | """Initialize the application components.""" 98 | try: 99 | logger.info("Initializing application components...") 100 | self.log_settings.apply() 101 | self._initialize_cli_components() 102 | logger.info("Application components initialized successfully") 103 | except Exception as e: 104 | logger.error(f"Error initializing application: {e}") 105 | raise 106 | 107 | def _initialize_cli_components(self) -> None: 108 | """Initialize CLI components and managers.""" 109 | self.log_settings_cli.setup_commands() 110 | self._setup_cli_commands() 111 | 112 | def _setup_cli_commands(self) -> None: 113 | """Set up CLI commands and options.""" 114 | self.file_select_cli.add_command(self.mmio_select_cli.cli_commands) 115 | self.file_select_cli.add_command(self.output_select_cli.cli_commands) 116 | self.file_select_cli.add_command(self.log_settings_cli.cli_commands) 117 | 118 | if self.settings_path: 119 | self.file_select_cli.add_command(click.Command(name=self.settings_path.name)) 120 | if self.settings_output_path: 121 | self.file_select_cli.add_command(click.Command(name=self.settings_output_path.name)) 122 | if self.settings_module_header: 123 | self.file_select_cli.add_command(click.Command(name=self.settings_module_header)) 124 | 125 | def run(self) -> None: 126 | """Run the application flow.""" 127 | try: 128 | logger.info("Starting application flow...") 129 | 130 | logger.info("Initializing components") 131 | self.initialize() 132 | 133 | logger.info("Displaying welcome message") 134 | self._show_welcome() 135 | 136 | logger.info("Processing MMIO file selection") 137 | mmio_file = self.mmio_cli_manager.process_cli_selection() 138 | if not mmio_file: 139 | logger.error("No MMIO file selected") 140 | return 141 | 142 | logger.info(f"Selected MMIO file: {mmio_file}") 143 | 144 | logger.info("Collecting CLI options") 145 | cli_options = self._collect_cli_options() 146 | logger.info(f"Collected CLI options: {cli_options}") 147 | 148 | logger.info("Setting up orchestrator with selected file") 149 | logger.info(f"Setting input path: {mmio_file.parent}") 150 | logger.info(f"Setting input file name: {mmio_file.name}") 151 | 152 | self.orchestrator.input_orch.mmio_file_manager.input_manager.file_manager.path = mmio_file.parent 153 | self.orchestrator.input_orch.mmio_file_manager.input_manager.file_manager.file_name = mmio_file.name 154 | 155 | logger.info( 156 | f"Orchestrator input manager state - input_path: " 157 | f"{self.orchestrator.input_orch.mmio_file_manager.input_manager.file_manager.path}, input_file_name: " 158 | f"{self.orchestrator.input_orch.mmio_file_manager.input_manager.file_manager.file_name}" 159 | ) 160 | 161 | logger.info("Running orchestrator") 162 | self.orchestrator.run(which_steps=["input", "build", "output"], cli_options=cli_options) 163 | 164 | logger.info("Application flow completed successfully") 165 | 166 | except Exception as e: 167 | logger.error(f"Error in application flow: {e}", exc_info=True) 168 | click.echo(f"Error: {e}") 169 | 170 | def _show_welcome(self) -> None: 171 | """Display welcome message and initial menu.""" 172 | logger.info("Showing welcome message and menu") 173 | 174 | title = Text("MMIO to Verilog Generator", style="bold blue") 175 | title.align("center", width=60) 176 | 177 | version = Text("v0.0.1", style="cyan") 178 | version.align("center", width=60) 179 | 180 | menu_text = Text() 181 | menu_text.append("\n📁 1. Select a file", style="green") 182 | menu_text.append("\n🔍 2. Configure logging", style="green") 183 | 184 | panel_content = Text() 185 | panel_content.append(title) 186 | panel_content.append("\n\n") 187 | panel_content.append(version) 188 | panel_content.append("\n\n") 189 | panel_content.append(menu_text) 190 | 191 | console.print( 192 | Panel( 193 | panel_content, 194 | box=box.ROUNDED, 195 | padding=(1, 2), 196 | title="https://simonrak.gay", 197 | title_align="center", 198 | width=60, 199 | ) 200 | ) 201 | 202 | try: 203 | choice = click.prompt("Enter your choice", type=int) 204 | logger.info(f"User selected option: {choice}") 205 | 206 | if choice == 1: 207 | logger.info("Initializing file selection") 208 | self.mmio_cli_manager.setup_cli() 209 | console.print("[green]File selection initialized.[/green]") 210 | elif choice == 2: 211 | logger.info("Configuring logging settings") 212 | self.log_settings_cli.prompt_user() 213 | else: 214 | logger.warning(f"Invalid choice: {choice}") 215 | console.print("[red]Invalid choice. Please try again.[/red]") 216 | except ValueError: 217 | logger.warning("Invalid input type provided") 218 | console.print("[red]Invalid input. Please enter a number.[/red]") 219 | 220 | def _collect_cli_options(self) -> CLIOptions: 221 | """Collect CLI options for Verilog generation.""" 222 | console.print( 223 | Panel( 224 | "\n[bold blue]Verilog Generation Options[/bold blue]\n", 225 | box=box.ROUNDED, 226 | title="Configuration", 227 | title_align="center", 228 | width=60, 229 | ) 230 | ) 231 | 232 | console.print("[cyan]BAR Configuration[/cyan]") 233 | bar_input = click.prompt( 234 | "Enter BAR numbers separated by commas (or type 'all' to process all BARs)", 235 | default="all", 236 | ) 237 | if bar_input.strip().lower() == "all": 238 | bar_selection = None 239 | console.print("[green]✓[/green] Processing all BARs") 240 | else: 241 | try: 242 | bar_selection = [int(b.strip()) for b in bar_input.split(",")] 243 | console.print(f"[green]✓[/green] Selected BARs: {bar_selection}") 244 | except ValueError: 245 | console.print("[yellow]⚠[/yellow] Invalid BAR input, processing all BARs") 246 | bar_selection = None 247 | 248 | console.print("\n[cyan]Operation Type[/cyan]") 249 | op_filter = click.prompt("Select operation type: [R]ead, [W]rite, [B]oth", default="B").upper() 250 | console.print(f"[green]✓[/green] Selected operation type: {op_filter}") 251 | 252 | console.print("\n[cyan]Function Generation Options[/cyan]") 253 | include_address_checks = click.confirm("🔍 Generate Address Check functions?", default=True) 254 | include_counters = click.confirm("🔢 Generate Counter functions?", default=True) 255 | include_logic = click.confirm("⚡ Generate Logic functions?", default=True) 256 | include_state_machines = click.confirm("🔄 Generate State Machine functions?", default=True) 257 | init_roms = click.confirm("💾 Generate ROM Initialization functions?", default=True) 258 | 259 | options = CLIOptions( 260 | bar_selection=bar_selection, 261 | operation_filter=op_filter, 262 | include_address_checks=include_address_checks, 263 | include_counters=include_counters, 264 | include_logic=include_logic, 265 | include_state_machines=include_state_machines, 266 | init_roms=init_roms, 267 | ) 268 | 269 | console.print("\n[bold green]Configuration Complete![/bold green]") 270 | console.print(f"Selected BARs: {options.bar_selection}") 271 | console.print(f"Operation Type: {options.operation_filter}") 272 | console.print(f"Include Address Checks: {options.include_address_checks}") 273 | console.print(f"Include Counters: {options.include_counters}") 274 | console.print(f"Include Logic: {options.include_logic}") 275 | console.print(f"Include State Machines: {options.include_state_machines}") 276 | console.print(f"Include ROMs: {options.init_roms}") 277 | console.print( 278 | f"\n[bold green]File saved to: {self.verilog_settings.file_output_path}/{self.file_name}[/bold green]" 279 | ) 280 | return options 281 | 282 | def get_settings(self) -> tuple[MMIOSettings, VerilogSettings]: 283 | """Get all collected settings.""" 284 | return self.mmio_settings, self.verilog_settings 285 | 286 | def get_generator_options(self) -> CLIOptions: 287 | """Collect additional CLI options for Verilog generation.""" 288 | bar_input = click.prompt( 289 | "Enter BAR numbers separated by commas (or type 'all' to process all BARs)", 290 | default="all", 291 | ) 292 | if bar_input.strip().lower() == "all": 293 | bar_selection = None 294 | else: 295 | try: 296 | bar_selection = [int(b.strip()) for b in bar_input.split(",")] 297 | except ValueError: 298 | click.echo("Invalid input for BAR numbers. Processing all BARs by default.") 299 | bar_selection = None 300 | 301 | op_filter = click.prompt("Select operation type: R for Read, W for Write, B for Both", default="B").upper() 302 | 303 | include_address_checks = click.confirm("Generate Address Check functions?", default=True) 304 | include_counters = click.confirm("Generate Counter functions?", default=True) 305 | include_defaults = click.confirm("Generate Default Values?", default=True) 306 | 307 | return CLIOptions( 308 | bar_selection=bar_selection, 309 | operation_filter=op_filter, 310 | include_address_checks=include_address_checks, 311 | include_counters=include_counters, 312 | include_default_values=include_defaults, 313 | ) 314 | 315 | def run_application(self) -> None: 316 | """Central method to run the full flow from CLI input to Verilog generation.""" 317 | self._show_welcome() 318 | 319 | cli_coordinator = CLICoordinator() 320 | generator_options: CLIOptions = cli_coordinator.collect_options() 321 | 322 | modular_orch = ModularOrchestrator() 323 | 324 | modular_orch.run(which_steps=["input", "build", "output"], cli_options=generator_options) 325 | -------------------------------------------------------------------------------- /mmio/application/verilog/verilog_builder_orchestrator.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from io import StringIO 3 | from typing import Any 4 | 5 | from pydantic import BaseModel, ConfigDict, Field 6 | 7 | from mmio.application.verilog.generators.address_check import AddressCheckGenerator 8 | from mmio.application.verilog.generators.counter_generator import CounterGenerator 9 | from mmio.application.verilog.generators.response_logic import ResponseLogicGenerator 10 | from mmio.application.verilog.generators.rom import ROMGenerator 11 | from mmio.application.verilog.generators.static_generator import StaticCodeGenerator 12 | from mmio.application.verilog.verilog_models import ( 13 | AddressCheckEntry, 14 | CounterEntry, 15 | LogicEntries, 16 | ROMEntry, 17 | VerilogGenerator, 18 | VerilogStatic, 19 | ) 20 | from mmio.core.logger import LoggerManager 21 | from mmio.domain.models.verilog_data import VerilogData 22 | 23 | logger = LoggerManager.get_logger(__name__) 24 | 25 | 26 | class GeneratorType(Enum): 27 | """Enum for generator types.""" 28 | 29 | ROM = "rom" 30 | COUNTER = "counter" 31 | ADDRESS_CHECK = "addr_check" 32 | HEADER = "header" 33 | STATE_MACHINE_START = "state_machine_start" 34 | STATE_MACHINE_END = "state_machine_end" 35 | LOGIC = "logic" 36 | RESET_COUNTER = "reset_counter" 37 | ROM_INIT = "rom_init" 38 | 39 | 40 | class VerilogBuilderOrchestrator(BaseModel): 41 | """Enhanced Verilog Builder with dynamic generator support.""" 42 | 43 | model_config = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True) 44 | 45 | generator_classes: dict[str, type[VerilogGenerator]] = Field(default_factory=dict) 46 | generator_instances: dict[str, VerilogGenerator] = Field(default_factory=dict) 47 | verilog_data: VerilogData = Field(default_factory=VerilogData) 48 | 49 | bar_number: int = Field(default=0) 50 | 51 | verilog_header: VerilogStatic = Field( 52 | default_factory=lambda: VerilogStatic( 53 | bar_number=0, 54 | verilog_header=StringIO(), 55 | verilog_state_machine_start=StringIO(), 56 | verilog_state_machine_end=StringIO(), 57 | ) 58 | ) 59 | 60 | verilog_rom: ROMEntry = Field( 61 | default_factory=lambda: ROMEntry( 62 | bar_number=0, 63 | verilog_read_rom=StringIO(), 64 | verilog_write_rom=StringIO(), 65 | verilog_read_rom_init=StringIO(), 66 | verilog_write_rom_init=StringIO(), 67 | ) 68 | ) 69 | 70 | verilog_counter: CounterEntry = Field( 71 | default_factory=lambda: CounterEntry( 72 | bar_number=0, 73 | verilog_read_counter=StringIO(), 74 | verilog_write_counter=StringIO(), 75 | verilog_reset_read_counter=StringIO(), 76 | verilog_reset_write_counter=StringIO(), 77 | ) 78 | ) 79 | 80 | verilog_address_check: AddressCheckEntry = Field( 81 | default_factory=lambda: AddressCheckEntry( 82 | bar_number=0, 83 | verilog_read_addr_check=StringIO(), 84 | verilog_write_addr_check=StringIO(), 85 | ) 86 | ) 87 | 88 | verilog_logic: LogicEntries = Field( 89 | default_factory=lambda: LogicEntries( 90 | bar_number=0, 91 | verilog_read_cases=StringIO(), 92 | verilog_write_cases=StringIO(), 93 | ) 94 | ) 95 | 96 | def __init__(self, **data: Any): 97 | """Initialize the VerilogBuilderOrchestrator with registered generators.""" 98 | super().__init__(**data) 99 | logger.info("Initializing VerilogBuilderOrchestrator") 100 | 101 | # Register default generators 102 | self.register_generator(GeneratorType.ROM.value, ROMGenerator) 103 | self.register_generator(GeneratorType.ADDRESS_CHECK.value, AddressCheckGenerator) 104 | self.register_generator(GeneratorType.COUNTER.value, CounterGenerator) 105 | self.register_generator(GeneratorType.RESET_COUNTER.value, CounterGenerator) 106 | self.register_generator(GeneratorType.HEADER.value, StaticCodeGenerator) 107 | self.register_generator(GeneratorType.STATE_MACHINE_START.value, StaticCodeGenerator) 108 | self.register_generator(GeneratorType.STATE_MACHINE_END.value, StaticCodeGenerator) 109 | self.register_generator(GeneratorType.LOGIC.value, ResponseLogicGenerator) 110 | self.register_generator(GeneratorType.ROM_INIT.value, ROMGenerator) 111 | 112 | logger.info(f"Registered generators: {list(self.generator_classes.keys())}") 113 | 114 | def set_verilog_data(self, mmio_data: VerilogData) -> None: 115 | """Set VerilogData for all generators.""" 116 | logger.info("Setting VerilogData in orchestrator") 117 | 118 | # Set the current VerilogData instance 119 | self.verilog_data = mmio_data 120 | 121 | # Update all generator instances with the data 122 | for generator in self.generator_instances.values(): 123 | addresses = VerilogData.addresses(bar=generator.bar_number) 124 | bit_widths = VerilogData.get_bar_address_bit_widths(generator.bar_number) 125 | read_values = VerilogData.read_values(bar=generator.bar_number) 126 | write_values = VerilogData.write_values(bar=generator.bar_number) 127 | defaults = VerilogData.get_default_values(generator.bar_number) 128 | 129 | generator.set_bar_data( 130 | addresses=addresses, 131 | bit_widths=bit_widths, 132 | read_values=read_values, 133 | write_values=write_values, 134 | defaults=defaults, 135 | ) 136 | 137 | @property 138 | def available_bars(self) -> list[int]: 139 | """Get list of available BAR numbers.""" 140 | return sorted(bar for generator in self.generator_instances.values() for bar in generator.get_available_bars()) 141 | 142 | @property 143 | def supported_generators(self) -> list[str]: 144 | """Get list of supported generator types.""" 145 | return [e.value for e in GeneratorType] 146 | 147 | def register_generator(self, generator_type: str, generator_class: type[VerilogGenerator]) -> None: 148 | """Register a new generator type.""" 149 | if generator_type not in [e.value for e in GeneratorType]: 150 | raise ValueError(f"Invalid generator type: {generator_type}") 151 | self.generator_classes[generator_type] = generator_class 152 | 153 | def _validate_verilog_data(self, bar_number: int) -> bool: 154 | """Validate VerilogData and required fields for generation.""" 155 | if not self.verilog_data: 156 | logger.warning("No VerilogData set, cannot generate code") 157 | return False 158 | 159 | bar_addresses = self.verilog_data.addresses(bar=bar_number) 160 | address_bit_widths = self.verilog_data.get_bar_address_bit_widths(bar_number) 161 | 162 | logger.info(f"Found addresses for BAR: {bar_number}: {bar_addresses}") 163 | logger.info(f"Address bit widths: {address_bit_widths}") 164 | 165 | if not bar_addresses: 166 | logger.warning(f"No addresses found for BAR: {bar_number}") 167 | return False 168 | 169 | if not address_bit_widths: 170 | logger.warning(f"No bit width information for BAR: {bar_number}") 171 | return False 172 | 173 | return True 174 | 175 | def _get_generator_instance(self, field_type: str, bar_number: int) -> VerilogGenerator: 176 | """Get or create a generator instance for the given field type.""" 177 | if field_type not in self.generator_classes: 178 | raise ValueError(f"Unknown field type: {field_type}") 179 | 180 | generator = self.generator_classes[field_type](bar_number=bar_number) 181 | self.generator_instances[field_type] = generator 182 | 183 | if hasattr(generator, "set_bar_data"): 184 | generator.set_bar_data( 185 | addresses=self.verilog_data.addresses(bar=bar_number), 186 | bit_widths=self.verilog_data.get_bar_address_bit_widths(bar_number), 187 | read_values=self.verilog_data.read_values(bar=bar_number), 188 | write_values=self.verilog_data.write_values(bar=bar_number), 189 | defaults=self.verilog_data.get_default_values(bar_number), 190 | ) 191 | 192 | return generator 193 | 194 | def _get_rom_entry(self, bar_number: int) -> ROMEntry: 195 | """Get or create a ROM entry for the given BAR number.""" 196 | rom_gen = self.generator_instances.get("rom") 197 | rom_entry = ROMEntry(bar_number=bar_number) 198 | if isinstance(rom_gen, ROMGenerator): 199 | rom_gen.generate_rom_structure(bar_number) 200 | if rom_gen.verilog_rom is not None: 201 | rom_entry = rom_gen.verilog_rom 202 | return rom_entry 203 | 204 | def _get_counter_entry(self, bar_number: int) -> CounterEntry: 205 | """Get or create a counter entry for the given BAR number.""" 206 | counter_gen = self.generator_instances.get("counter") 207 | counter_entry = CounterEntry(bar_number=bar_number) 208 | if isinstance(counter_gen, CounterGenerator): 209 | counter_gen.generate_verilog_counter(bar_number) 210 | if counter_gen.verilog_counter is not None: 211 | counter_entry = counter_gen.verilog_counter 212 | return counter_entry 213 | 214 | def _get_operation_value(self, operation: str | None, read_value: StringIO, write_value: StringIO) -> str: 215 | """Get the appropriate value based on the operation.""" 216 | return str(read_value.getvalue() if operation == "read" else write_value.getvalue()) 217 | 218 | def _handle_header(self, generator: StaticCodeGenerator) -> str: 219 | """Handle header field type generation.""" 220 | header = generator.generate_module_header() 221 | self.verilog_header = header 222 | return str(self.verilog_header.verilog_header.getvalue()) 223 | 224 | def _handle_state_machine_start(self, generator: StaticCodeGenerator) -> str: 225 | """Handle state machine start field type generation.""" 226 | start = generator.generate_state_machine_start() 227 | self.verilog_header = start 228 | return str(self.verilog_header.verilog_state_machine_start.getvalue()) 229 | 230 | def _handle_state_machine_end(self, generator: StaticCodeGenerator) -> str: 231 | """Handle state machine end field type generation.""" 232 | end = generator.generate_state_machine_end() 233 | self.verilog_header = end 234 | return str(self.verilog_header.verilog_state_machine_end.getvalue()) 235 | 236 | def _handle_rom(self, generator: ROMGenerator, operation: str | None) -> str: 237 | """Handle ROM field type generation.""" 238 | generator.generate_rom_structure(self.bar_number) 239 | self.verilog_rom = generator.verilog_rom 240 | return self._get_operation_value( 241 | operation, self.verilog_rom.verilog_read_rom, self.verilog_rom.verilog_write_rom 242 | ) 243 | 244 | def _handle_rom_init(self, generator: ROMGenerator, operation: str | None) -> str: 245 | """Handle ROM init field type generation.""" 246 | generator.generate_rom_init(self.bar_number) 247 | self.verilog_rom = generator.verilog_rom 248 | 249 | return str( 250 | self.verilog_rom.verilog_read_rom_init.getvalue() 251 | if operation == "read" 252 | else self.verilog_rom.verilog_write_rom_init.getvalue() 253 | ) 254 | 255 | def _handle_reset_counter(self, generator: CounterGenerator, operation: str | None) -> str: 256 | """Handle reset counter field type generation.""" 257 | generator.generate_reset_counter(self.bar_number) 258 | self.verilog_counter = generator.verilog_counter 259 | 260 | return str( 261 | self.verilog_counter.verilog_reset_read_counter.getvalue() 262 | if operation == "read" 263 | else self.verilog_counter.verilog_reset_write_counter.getvalue() 264 | ) 265 | 266 | def _handle_counter(self, generator: CounterGenerator, operation: str | None) -> str: 267 | """Handle counter field type generation.""" 268 | self.verilog_counter = generator.generate_verilog_counter(self.bar_number) 269 | return self._get_operation_value( 270 | operation, 271 | self.verilog_counter.verilog_read_counter, 272 | self.verilog_counter.verilog_write_counter, 273 | ) 274 | 275 | def _handle_addr_check(self, generator: AddressCheckGenerator, operation: str | None) -> str: 276 | """Handle address check field type generation.""" 277 | self.verilog_address_check = generator.generate_address_check_entry(self.bar_number) 278 | return self._get_operation_value( 279 | operation, 280 | self.verilog_address_check.verilog_read_addr_check, 281 | self.verilog_address_check.verilog_write_addr_check, 282 | ) 283 | 284 | def _handle_logic(self, generator: ResponseLogicGenerator, operation: str | None) -> str: 285 | """Handle logic field type generation.""" 286 | rom_entry = self._get_rom_entry(self.bar_number) 287 | counter_entry = self._get_counter_entry(self.bar_number) 288 | self.verilog_logic = generator.generate_verilog_logic(self.bar_number, rom_entry, counter_entry) 289 | return self._get_operation_value( 290 | operation, 291 | self.verilog_logic.verilog_read_cases, 292 | self.verilog_logic.verilog_write_cases, 293 | ) 294 | 295 | def build_verilog(self, operation: str | None, bar_number: int, field_type: str) -> str: 296 | """Generate Verilog code using the class's BAR number.""" 297 | self.bar_number = bar_number 298 | logger.info(f"Building for BAR: {self.bar_number} with {len(VerilogData.get_all_instances())} entries") 299 | 300 | for generator in self.generator_instances.values(): 301 | generator.bar_number = self.bar_number 302 | logger.info(f"Updated {type(generator).__name__} to BAR: {self.bar_number}") 303 | 304 | if not self._validate_verilog_data(bar_number): 305 | return "" 306 | 307 | try: 308 | generator = self._get_generator_instance(field_type, bar_number) 309 | 310 | match field_type: 311 | case "header" if isinstance(generator, StaticCodeGenerator): 312 | return self._handle_header(generator) 313 | 314 | case "state_machine_start" if isinstance(generator, StaticCodeGenerator): 315 | return self._handle_state_machine_start(generator) 316 | 317 | case "state_machine_end" if isinstance(generator, StaticCodeGenerator): 318 | return self._handle_state_machine_end(generator) 319 | 320 | case "rom" if isinstance(generator, ROMGenerator): 321 | return self._handle_rom(generator, operation) 322 | 323 | case "rom_init" if isinstance(generator, ROMGenerator): 324 | return self._handle_rom_init(generator, operation) 325 | 326 | case "counter" if isinstance(generator, CounterGenerator): 327 | return self._handle_counter(generator, operation) 328 | 329 | case "reset_counter" if isinstance(generator, CounterGenerator): 330 | return self._handle_reset_counter(generator, operation) 331 | 332 | case "addr_check" if isinstance(generator, AddressCheckGenerator): 333 | return self._handle_addr_check(generator, operation) 334 | 335 | case "logic" if isinstance(generator, ResponseLogicGenerator): 336 | return self._handle_logic(generator, operation) 337 | 338 | case _: 339 | logger.warning(f"No handler found for field type: {field_type}") 340 | return "" 341 | 342 | except ValueError as e: 343 | logger.warning(str(e)) 344 | return "" 345 | --------------------------------------------------------------------------------