Source code for ska_low_cbf_fpga.args_map

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")