465 lines
17 KiB
Python
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
|