import bisect
import dataclasses
import os
import typing
from dataclasses import dataclass
from importlib.util import module_from_spec, spec_from_file_location
from ska_low_cbf_fpga.args_fpga import WORD_SIZE
"""
ARGS FPGA map file handling
"""
[docs]@dataclass
class ArgsFieldInfo:
# descriptions are often quite long... worth keeping?
description: str = dataclasses.field(default=None)
address: int = dataclasses.field(default=None)
length: int = dataclasses.field(default=1)
bit_offset: int = dataclasses.field(default=None)
width: int = dataclasses.field(default=None)
[docs]def load_map(build: int, map_dir: str):
"""
Load FPGA register map file from a directory.
Looks for a file named with the build date of the running firmware image.
:param build: Hexadecimal representation of this is used to select filename,
``fpgamap_<build>.py``
:param map_dir: Directory containing the map file. No other paths are searched!
"""
map_file_name = f"fpgamap_{build:x}.py"
map_file_path = os.path.join(map_dir, map_file_name)
return load_fpgamap_from_file(map_file_path)
[docs]def load_fpgamap_from_file(map_file_path: str) -> dict:
"""
Load the FPGAMAP variable from a specified python file.
:param map_file_path: path to .py file containing FPGAMAP dict
"""
spec = spec_from_file_location(map_file_path, map_file_path)
map_module = module_from_spec(spec)
spec.loader.exec_module(map_module)
return map_module.FPGAMAP
[docs]class ArgsMap(dict):
"""
ARGS FPGA map file decoder.
"""
[docs] @classmethod
def create_from_file(cls, build: int, map_dir: str):
"""
:param build: ARGS map build timestamp
:param map_dir: directory to find ARGS map file in
"""
return cls(load_map(build, map_dir))
[docs] def __init__(self, spec: dict):
"""
:param spec: ARGS register map
"""
super().__init__()
self._locked = False
self._decode_map(spec)
self._locked = True
[docs] def __setitem__(self, key, value):
"""
Should only be called internally, during object creation.
:param key: peripheral name
:param value: dict of ArgsFieldInfo
:raises NotImplementedError: if called after ArgsMap init
"""
if self._locked:
raise NotImplementedError(
f"{self.__class__.__name__} items are set on creation"
)
super().__setitem__(key, value)
[docs] def _decode_map(self, fpga_map: dict):
"""
Given an FPGAMAP nested dictionary structure, extract relevant info for its
RAM and Register fields.
"""
for p_name, peripheral in fpga_map.items():
if p_name not in self:
self[p_name] = {}
for s_name, slave in peripheral["slaves"].items():
if slave["type"] in ["REG", "RAM"]:
for f_name, field in slave["fields"].items():
field_address = (
peripheral["start"]
+ slave["start"]
+ field["start"]
) * WORD_SIZE
length = field["stop"] - field["start"]
# "width" from ARGS is bits.
self[p_name][f_name] = ArgsFieldInfo(
address=field_address,
length=length,
description=field["description"],
bit_offset=field["bit_offset"],
width=field["width"],
)
[docs]@dataclass
class AddressInfo:
"""
Minimal set of information about an FPGA register address.
(a collection of these is used for address number -> name lookups)
"""
name: str
length: int
[docs]def all_register_addresses(fpga_map: dict) -> typing.Dict[int, AddressInfo]:
"""
Creates a lookup table that can be used to convert byte based addresses to
full register names.
:param fpga_map: FPGAMAP object
:return: e.g. ``{40: "fpga.system.system.time_uptime"}``
"""
registers = dict()
for p_name, peripheral in fpga_map.items():
for s_name, slave in peripheral["slaves"].items():
for f_name, field in slave["fields"].items():
field_address = (
peripheral["start"] + slave["start"] + field["start"]
) * WORD_SIZE
length = field["stop"] - field["start"]
registers[field_address] = AddressInfo(
name=f"{p_name}.{s_name}.{f_name}", length=length
)
return dict(sorted(registers.items()))
[docs]def get_name_and_offset(
registers: typing.Dict[int, AddressInfo], address: int
) -> (int, int):
"""
Get register name and offset into register for a given address.
Looks up the nearest lesser address and finds the relative offset.
Checks that this is within the defined register length.
:param registers: use :py:func:`all_register_addresses` to create this
:param address: address to look up
:return: (name, word offset) - e.g. ``("array.my_array.data", 4)``
:raises ValueError: if not a valid address
"""
if address in registers:
# exact address match found (zero offset)
return registers[address].name, 0
# the below logic works for the general case, but we expect
# that most lookups will be an exact match and the above should be faster
keys = list(registers.keys())
index = bisect.bisect_right(keys, address)
if index:
index -= 1
less_addr = keys[index]
less_reg = registers[less_addr]
byte_offset = address - less_addr
word_offset = byte_offset // 4
if byte_offset % 4:
raise ValueError(f"Address {address} is not on a word boundary")
if word_offset > less_reg.length:
raise ValueError(
f"Address {address} is beyond extent of "
f"{less_reg.name} ({less_reg.length} words)"
)
return less_reg.name, word_offset
else:
raise ValueError(f"No register address less than {address} exists")