SXXXXXXX_PyDownloadFwViaSRIO/pydownloadfwviasrio/profiles.py
2026-01-22 17:10:05 +01:00

465 lines
17 KiB
Python

"""Flash profile management for multi-target systems.
This module provides classes to manage flash profiles (targets) with
different SRIO endpoints, IP addresses, and flash configurations.
The structure follows a three-level hierarchy:
1. GlobalConfig: System-wide settings (IP, port, SRIO base, etc.)
2. FlashModel: Reusable FPGA model definitions (memory map, sectors, etc.)
3. FlashTarget: Specific targets referencing a model by ID
Reference: Vecchia_app FpgaFlashProfile system and targets.ini format.
"""
from __future__ import annotations
import configparser
import json
from dataclasses import dataclass, asdict, field
from pathlib import Path
from typing import List, Optional, Dict
@dataclass
class GlobalConfig:
"""Global system configuration.
Attributes:
ip: Default IP address for SRIO communication.
port: Default port for TFTP/SRIO communication.
srio_base: SRIO base address (hex value).
fpga_base: FPGA base address (hex value).
fpga_sector: FPGA sector size (hex value).
smart: Smart mode flag (0 or 1).
section: Default flash section (e.g., "APP1").
default_target: Default target ID to use.
"""
ip: str = "192.168.2.102"
port: int = 50069
srio_base: int = 0x500000
fpga_base: int = 0x0
fpga_sector: int = 0x010000
smart: int = 1
section: str = "APP1"
default_target: str = "AESA_RFIF"
verify_after_write: bool = False # Read-back verification after each chunk write
# Retry configuration (at different levels)
max_tftp_retries: int = 3 # TFTP transport level (SimpleTFTPClient)
max_register_retries: int = 500 # Register write verification level (srio_flash.py)
max_chunk_write_retries: int = 3 # Flash chunk write+verify level (core.py)
def to_dict(self) -> dict:
"""Convert to dictionary for JSON serialization."""
return asdict(self)
@staticmethod
def from_dict(data: dict) -> GlobalConfig:
"""Create GlobalConfig from dictionary."""
return GlobalConfig(**data)
@dataclass
class FlashModel:
"""FPGA flash memory model definition.
Attributes:
id_model: Unique model identifier (e.g., 0, 1, 2).
model: Model name (e.g., "xcku040", "rfif").
description: Human-readable description.
flash_type: Flash type identifier (0 or 1).
is_4byte_addressing: True if 4-byte addressing, False for 3-byte.
num_sectors: Total number of flash sectors.
golden_start: Golden area start address.
golden_stop: Golden area stop address.
user_start: User area start address.
user_stop: User area stop address.
test_address: Optional test address for verification.
"""
id_model: int
model: str
description: str
flash_type: int
is_4byte_addressing: bool
num_sectors: int
golden_start: int
golden_stop: int
user_start: int
user_stop: int
test_address: Optional[int] = None
def to_dict(self) -> dict:
"""Convert to dictionary for JSON serialization."""
return asdict(self)
@staticmethod
def from_dict(data: dict) -> FlashModel:
"""Create FlashModel from dictionary."""
return FlashModel(**data)
@dataclass
class FlashTarget:
"""Individual flash target configuration.
Attributes:
id_target: Unique target identifier (e.g., "EIF_FPGA1").
description: Human-readable description.
slot_address: SRIO slot/endpoint address (hex value).
architecture: Target architecture (e.g., "Xilinx", "RFIF").
name: Target name (same as id_target usually).
file_prefix: Prefix for firmware files.
id_model: Reference to FlashModel ID.
golden_binary_path: Optional path to golden area firmware binary.
user_binary_path: Optional path to user area firmware binary.
"""
id_target: str
description: str
slot_address: int
architecture: str
name: str
file_prefix: str
id_model: int
golden_binary_path: Optional[str] = None
user_binary_path: Optional[str] = None
def to_dict(self) -> dict:
"""Convert to dictionary for JSON serialization."""
return asdict(self)
@staticmethod
def from_dict(data: dict) -> FlashTarget:
"""Create FlashTarget from dictionary."""
return FlashTarget(**data)
# Legacy class for backward compatibility
@dataclass
class FlashProfile:
"""Legacy flash target configuration (deprecated).
This class is kept for backward compatibility with older code.
New code should use FlashTarget + FlashModel instead.
Attributes:
name: Human-readable profile name (e.g., "Primary Flash", "Secondary Flash").
slot_address: SRIO endpoint/slot address (hex string, e.g., "0x10").
ip: Target IP address (e.g., "192.168.1.100").
port: TFTP port on target (default 69).
base_address: Flash base address (hex, e.g., 0x01000000).
size: Total flash size in bytes (e.g., 16777216 for 16 MB).
binary_path: Optional path to binary file associated with this profile.
"""
name: str
slot_address: str
ip: str
port: int = 69
base_address: int = 0x01000000
size: int = 16 * 1024 * 1024 # 16 MB default
binary_path: Optional[str] = None
def to_dict(self) -> dict:
"""Convert to dictionary for JSON serialization."""
return asdict(self)
@staticmethod
def from_dict(data: dict) -> FlashProfile:
"""Create FlashProfile from dictionary."""
return FlashProfile(**data)
class ProfileManager:
"""Manages flash targets, models, and global configuration.
This manager handles the complete flash programming configuration including:
- Global system settings (IP, port, SRIO addresses)
- Flash models (FPGA types with memory maps)
- Flash targets (specific FPGA instances)
Supports both JSON and INI file formats for import/export.
"""
def __init__(self, config_path: Optional[Path] = None) -> None:
"""Initialize ProfileManager.
Args:
config_path: Path to JSON configuration file. Defaults to "flash_profiles.json".
"""
self.global_config = GlobalConfig()
self.models: Dict[int, FlashModel] = {}
self.targets: Dict[str, FlashTarget] = {}
self.config_path = config_path or Path("flash_profiles.json")
if self.config_path.exists():
self.load()
def add_model(self, model: FlashModel) -> None:
"""Add or update a flash model."""
self.models[model.id_model] = model
def add_target(self, target: FlashTarget) -> None:
"""Add or update a flash target."""
self.targets[target.id_target] = target
def get_model(self, id_model: int) -> Optional[FlashModel]:
"""Get model by ID."""
return self.models.get(id_model)
def get_target(self, id_target: str) -> Optional[FlashTarget]:
"""Get target by ID."""
return self.targets.get(id_target)
def get_target_with_model(self, id_target: str) -> Optional[tuple[FlashTarget, FlashModel]]:
"""Get target together with its associated model.
Args:
id_target: Target ID to retrieve.
Returns:
Tuple of (FlashTarget, FlashModel) if found, None otherwise.
"""
target = self.get_target(id_target)
if not target:
return None
model = self.get_model(target.id_model)
if not model:
return None
return (target, model)
def delete_model(self, id_model: int) -> bool:
"""Delete model by ID. Returns True if found and deleted."""
if id_model in self.models:
del self.models[id_model]
return True
return False
def delete_target(self, id_target: str) -> bool:
"""Delete target by ID. Returns True if found and deleted."""
if id_target in self.targets:
del self.targets[id_target]
return True
return False
def list_models(self) -> List[int]:
"""Return list of model IDs."""
return list(self.models.keys())
def list_targets(self) -> List[str]:
"""Return list of target IDs."""
return list(self.targets.keys())
def save(self) -> None:
"""Save configuration to JSON file."""
data = {
"global_config": self.global_config.to_dict(),
"models": {k: v.to_dict() for k, v in self.models.items()},
"targets": {k: v.to_dict() for k, v in self.targets.items()},
}
with open(self.config_path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
def load(self) -> None:
"""Load configuration from JSON file."""
with open(self.config_path, "r", encoding="utf-8") as f:
data = json.load(f)
# Load global config
if "global_config" in data:
self.global_config = GlobalConfig.from_dict(data["global_config"])
# Load models
if "models" in data:
self.models = {
int(k): FlashModel.from_dict(v)
for k, v in data["models"].items()
}
# Load targets
if "targets" in data:
self.targets = {
k: FlashTarget.from_dict(v)
for k, v in data["targets"].items()
}
def load_from_ini(self, ini_path: Path) -> None:
"""Load configuration from INI file (targets.ini format).
Args:
ini_path: Path to INI configuration file.
"""
parser = configparser.ConfigParser()
parser.read(ini_path, encoding="utf-8")
# Parse global config from [default] section
if "default" in parser:
section = parser["default"]
self.global_config.ip = section.get("ip", self.global_config.ip).strip('"')
self.global_config.port = section.getint("port", self.global_config.port)
self.global_config.smart = section.getint("smart", self.global_config.smart)
self.global_config.section = section.get("section", self.global_config.section)
# Parse hex values
if "srio_base" in section:
self.global_config.srio_base = int(section["srio_base"].replace("$", ""), 16)
if "fpga_base" in section:
self.global_config.fpga_base = int(section["fpga_base"].replace("$", ""), 16)
if "fpga_sector" in section:
self.global_config.fpga_sector = int(section["fpga_sector"].replace("$", ""), 16)
if "defTarget" in section:
self.global_config.default_target = section["defTarget"]
# Parse models from [MODEL-x] sections
for section_name in parser.sections():
if section_name.startswith("MODEL-"):
section = parser[section_name]
id_model = section.getint("idModel")
model = FlashModel(
id_model=id_model,
model=section.get("model", ""),
description=section.get("description", ""),
flash_type=section.getint("type", 0),
is_4byte_addressing=bool(section.getint("3_4Byte", 0)),
num_sectors=section.getint("numSector", 0),
golden_start=int(section.get("goldenAddressStartArea", "0x0"), 16),
golden_stop=int(section.get("goldenAddressStopArea", "0x0"), 16),
user_start=int(section.get("userAddressStartArea", "0x0"), 16),
user_stop=int(section.get("userAddresStopArea", "0x0"), 16),
)
if "testAddress" in section:
model.test_address = int(section["testAddress"], 16)
self.add_model(model)
# Parse targets from [TGT-x] sections
for section_name in parser.sections():
if section_name.startswith("TGT-"):
section = parser[section_name]
id_target = section.get("idTgt", "")
target = FlashTarget(
id_target=id_target,
description=section.get("description", ""),
slot_address=int(section.get("slotAddress", "0x0"), 16),
architecture=section.get("arch", ""),
name=section.get("name", id_target),
file_prefix=section.get("filePrefix", id_target),
id_model=section.getint("idModel", 0),
)
self.add_target(target)
def export_to_ini(self, ini_path: Path) -> None:
"""Export configuration to INI file (targets.ini format).
Args:
ini_path: Path where to save INI file.
"""
parser = configparser.ConfigParser()
# Write [default] section
parser["default"] = {
"port": str(self.global_config.port),
"ip": f'"{self.global_config.ip}"',
"smart": str(self.global_config.smart),
"section": self.global_config.section,
"srio_base": f"$0x{self.global_config.srio_base:X}",
"fpga_base": f"$0x{self.global_config.fpga_base:X}",
"fpga_sector": f"0x{self.global_config.fpga_sector:06X}",
"defTarget": self.global_config.default_target,
}
# Write [MODEL-x] sections
for id_model, model in sorted(self.models.items()):
section_name = f"MODEL-{id_model}"
parser[section_name] = {
"idModel": str(model.id_model),
"model": model.model,
"description": model.description,
"type": str(model.flash_type),
"3_4Byte": "1" if model.is_4byte_addressing else "0",
"numSector": str(model.num_sectors),
"goldenAddressStartArea": f"0x{model.golden_start:08X}",
"goldenAddressStopArea": f"0x{model.golden_stop:08X}",
"userAddressStartArea": f"0x{model.user_start:08X}",
"userAddresStopArea": f"0x{model.user_stop:08X}",
}
if model.test_address is not None:
parser[section_name]["testAddress"] = f"0x{model.test_address:08X}"
# Write [TGT-x] sections
for idx, (id_target, target) in enumerate(sorted(self.targets.items())):
section_name = f"TGT-{idx}"
parser[section_name] = {
"idTgt": target.id_target,
"description": target.description,
"slotAddress": f"0x{target.slot_address:02X}",
"arch": target.architecture,
"name": target.name,
"filePrefix": target.file_prefix,
"idModel": str(target.id_model),
}
with open(ini_path, "w", encoding="utf-8") as f:
parser.write(f)
def create_default_profiles() -> List[FlashProfile]:
"""Create sample legacy flash profiles for testing (deprecated).
This function is kept for backward compatibility.
Use ProfileManager.load_from_ini() for new code.
"""
return [
FlashProfile(
name="Primary Flash",
slot_address="0x10",
ip="192.168.1.100",
port=69,
base_address=0x01000000,
size=16 * 1024 * 1024,
),
FlashProfile(
name="Secondary Flash",
slot_address="0x11",
ip="192.168.1.100",
port=69,
base_address=0x02000000,
size=16 * 1024 * 1024,
),
FlashProfile(
name="Localhost Emulator",
slot_address="0xFF",
ip="127.0.0.1",
port=6969,
base_address=0x00000000,
size=16 * 1024 * 1024,
),
]
def initialize_from_targets_ini(
ini_path: Path = Path("_OLD/Vecchia_app/FpgaBeamMeUp/targets.ini"),
json_path: Path = Path("flash_profiles.json")
) -> ProfileManager:
"""Initialize ProfileManager from legacy targets.ini file.
This function reads the original targets.ini and creates a new JSON
configuration file with all models and targets properly structured.
Args:
ini_path: Path to targets.ini file.
json_path: Path where to save JSON configuration.
Returns:
Initialized ProfileManager with all data loaded.
"""
manager = ProfileManager(config_path=json_path)
manager.load_from_ini(ini_path)
manager.save()
return manager