diff options
Diffstat (limited to 'mpm/python/usrp_mpm/periph_manager')
-rw-r--r-- | mpm/python/usrp_mpm/periph_manager/CMakeLists.txt | 13 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/periph_manager/base.py | 54 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/periph_manager/common.py | 11 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/periph_manager/x4xx.py | 1280 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/periph_manager/x4xx_clk_aux.py | 682 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/periph_manager/x4xx_clk_mgr.py | 780 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/periph_manager/x4xx_gps_mgr.py | 157 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/periph_manager/x4xx_mb_cpld.py | 204 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/periph_manager/x4xx_periphs.py | 1445 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/periph_manager/x4xx_reference_pll.py | 339 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/periph_manager/x4xx_rfdc_ctrl.py | 480 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/periph_manager/x4xx_rfdc_regs.py | 263 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/periph_manager/x4xx_sample_pll.py | 315 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/periph_manager/x4xx_update_cpld.py | 232 |
14 files changed, 6244 insertions, 11 deletions
diff --git a/mpm/python/usrp_mpm/periph_manager/CMakeLists.txt b/mpm/python/usrp_mpm/periph_manager/CMakeLists.txt index 747b8967a..1ca96f53e 100644 --- a/mpm/python/usrp_mpm/periph_manager/CMakeLists.txt +++ b/mpm/python/usrp_mpm/periph_manager/CMakeLists.txt @@ -1,5 +1,5 @@ # -# Copyright 2017-2018 Ettus Research, a National Instruments Company +# Copyright 2017-2019 Ettus Research, a National Instruments Company # # SPDX-License-Identifier: GPL-3.0-or-later # @@ -18,6 +18,17 @@ set(USRP_MPM_PERIPHMGR_FILES ${CMAKE_CURRENT_SOURCE_DIR}/e320_periphs.py ${CMAKE_CURRENT_SOURCE_DIR}/e31x.py ${CMAKE_CURRENT_SOURCE_DIR}/e31x_periphs.py + ${CMAKE_CURRENT_SOURCE_DIR}/x4xx.py + ${CMAKE_CURRENT_SOURCE_DIR}/x4xx_periphs.py + ${CMAKE_CURRENT_SOURCE_DIR}/x4xx_clk_aux.py + ${CMAKE_CURRENT_SOURCE_DIR}/x4xx_clk_mgr.py + ${CMAKE_CURRENT_SOURCE_DIR}/x4xx_sample_pll.py + ${CMAKE_CURRENT_SOURCE_DIR}/x4xx_reference_pll.py + ${CMAKE_CURRENT_SOURCE_DIR}/x4xx_update_cpld.py + ${CMAKE_CURRENT_SOURCE_DIR}/x4xx_gps_mgr.py + ${CMAKE_CURRENT_SOURCE_DIR}/x4xx_mb_cpld.py + ${CMAKE_CURRENT_SOURCE_DIR}/x4xx_rfdc_regs.py + ${CMAKE_CURRENT_SOURCE_DIR}/x4xx_rfdc_ctrl.py ${CMAKE_CURRENT_SOURCE_DIR}/sim.py ) list(APPEND USRP_MPM_FILES ${USRP_MPM_PERIPHMGR_FILES}) diff --git a/mpm/python/usrp_mpm/periph_manager/base.py b/mpm/python/usrp_mpm/periph_manager/base.py index 53426a615..35986a83c 100644 --- a/mpm/python/usrp_mpm/periph_manager/base.py +++ b/mpm/python/usrp_mpm/periph_manager/base.py @@ -170,6 +170,8 @@ class PeriphManagerBase(object): dboard_eeprom_symbols = "db[0,1]_eeprom" # symbol glob fox auxiliary boards auxboard_eeprom_symbols = "*aux_eeprom" + # List of discoverable features supported by a device. + discoverable_features = [] # Disable checks for unused args in the overridables, because the default @@ -504,7 +506,18 @@ class PeriphManagerBase(object): for name, path in eeprom_paths.items(): self.log.debug("Reading EEPROM info for %s...", name) if not path: - self.log.debug("Not present. Skipping board") + if "db" in name: + # In order to support having a single dboard in slot 1 + # with slot 0 empty on a x4xx, we pretend that there is + # a dummy "EmptyDaughterboard" here. + self.log.debug("Not present. Inserting dummy DB info") + result[name] = { + 'eeprom_md': {'serial': 'deadbee', 'pid': 0x0}, + 'eeprom_raw': [], + 'pid': 0x0 + } + else: + self.log.debug("Not present. Skipping board") continue try: eeprom_md, eeprom_rawdata = self._read_dboard_eeprom_data(path) @@ -743,7 +756,8 @@ class PeriphManagerBase(object): "Cannot run deinit(), device was never fully initialized!") return self.log.trace("Mboard deinit() called.") - for dboard in self.dboards: + for slot, dboard in enumerate(self.dboards): + self.log.trace("call deinit() on dBoard in slot {}".format(slot)) dboard.deinit() def tear_down(self): @@ -752,6 +766,8 @@ class PeriphManagerBase(object): deconstruction. """ self.log.trace("Teardown called for Peripheral Manager base.") + for each in self.dboards: + each.tear_down() ########################################################################### # RFNoC & Device Info @@ -895,6 +911,7 @@ class PeriphManagerBase(object): assert (len(metadata_l) == len(data_l)),\ "update_component arguments must be the same length" # Iterate through the components, updating each in turn + basepath = os.path.join(os.sep, "tmp", "uploads") for metadata, data in zip(metadata_l, data_l): id_str = metadata['id'] filename = os.path.basename(metadata['filename']) @@ -903,7 +920,7 @@ class PeriphManagerBase(object): id_str, self.updateable_components.keys() )) raise KeyError("Update component not implemented for {}".format(id_str)) - self.log.trace("Updating component: {}".format(id_str)) + self.log.trace("Downloading component: {}".format(id_str)) if 'md5' in metadata: given_hash = metadata['md5'] comp_hash = md5() @@ -920,10 +937,9 @@ class PeriphManagerBase(object): comp_hash, given_hash)) raise RuntimeError("Component file hash mismatch") else: - self.log.trace("Loading unverified {} image.".format( + self.log.trace("Downloading unhashed {} image.".format( id_str )) - basepath = os.path.join(os.sep, "tmp", "uploads") filepath = os.path.join(basepath, filename) if not os.path.isdir(basepath): self.log.trace("Creating directory {}".format(basepath)) @@ -931,9 +947,15 @@ class PeriphManagerBase(object): self.log.trace("Writing data to {}".format(filepath)) with open(filepath, 'wb') as comp_file: comp_file.write(data) + + # do the actual installation on the device + for metadata in metadata_l: + id_str = metadata['id'] + filename = os.path.basename(metadata['filename']) + filepath = os.path.join(basepath, filename) update_func = \ getattr(self, self.updateable_components[id_str]['callback']) - self.log.info("Updating component `%s'", id_str) + self.log.info("Installing component `%s'", id_str) update_func(filepath, metadata) return True @@ -950,7 +972,7 @@ class PeriphManagerBase(object): self.log.trace("Component info: {}".format(metadata)) # Convert all values to str return dict([a, str(x)] for a, x in metadata.items()) - # else: + self.log.trace("Component not found in updateable components: {}" .format(component_name)) return {} @@ -1202,3 +1224,21 @@ class PeriphManagerBase(object): "time_source": self.get_time_source(), "clock_source": self.get_clock_source(), } + + ########################################################################### + # Clock/Time API + ########################################################################### + def set_clock_source_out(self, enable=True): + """ + Allows routing the clock configured as source to the RefOut terminal. + """ + raise NotImplementedError("set_clock_source_out() not implemented.") + + ####################################################################### + # Discoverable Features + ####################################################################### + def supports_feature(self, query): + """ + Returns true if the queried feature is supported by a device. + """ + return query in self.discoverable_features diff --git a/mpm/python/usrp_mpm/periph_manager/common.py b/mpm/python/usrp_mpm/periph_manager/common.py index e09be835e..6ed7f6c53 100644 --- a/mpm/python/usrp_mpm/periph_manager/common.py +++ b/mpm/python/usrp_mpm/periph_manager/common.py @@ -10,7 +10,7 @@ Common code for all MPM devices import datetime from usrp_mpm.sys_utils.uio import UIO -class MboardRegsCommon(object): +class MboardRegsCommon: """ Parent class for mboard regs that are common between *all* MPM devices """ @@ -34,6 +34,10 @@ class MboardRegsCommon(object): MB_TIME_BASE_PERIOD_LO = 0x101C MB_TIME_BASE_PERIOD_HI = 0x1020 MB_TIMEKEEPER_OFFSET = 12 + # Timekeeper control words + MB_TIME_SET_NOW = 0x0001 + MB_TIME_SET_NEXT_PPS = 0x0002 + MB_TIME_SET_NEXT_SYNC = 0x0004 # Bitfield locations for the MB_RFNOC_INFO register. MB_RFNOC_INFO_PROTO_VER = 0 MB_RFNOC_INFO_CHDR_WIDTH = 16 @@ -179,12 +183,13 @@ class MboardRegsCommon(object): """ addr_lo = \ self.MB_TIME_EVENT_LO + tk_idx * self.MB_TIMEKEEPER_OFFSET - addr_hi = addr_lo + 4 + addr_hi = \ + self.MB_TIME_EVENT_HI + tk_idx * self.MB_TIMEKEEPER_OFFSET addr_ctrl = \ self.MB_TIME_CTRL + tk_idx * self.MB_TIMEKEEPER_OFFSET time_lo = ticks & 0xFFFFFFFF time_hi = (ticks >> 32) & 0xFFFFFFFF - time_ctrl = 0x2 if next_pps else 0x1 + time_ctrl = self.MB_TIME_SET_NEXT_PPS if next_pps else self.MB_TIME_SET_NOW self.log.trace("Setting time on timekeeper %d to %d %s", tk_idx, ticks, ("on next pps" if next_pps else "now")) with self.regs: diff --git a/mpm/python/usrp_mpm/periph_manager/x4xx.py b/mpm/python/usrp_mpm/periph_manager/x4xx.py new file mode 100644 index 000000000..b32cbeb08 --- /dev/null +++ b/mpm/python/usrp_mpm/periph_manager/x4xx.py @@ -0,0 +1,1280 @@ +# +# Copyright 2019 Ettus Research, a National Instruments Company +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +""" +X400 implementation module +""" + +import threading +import copy +from time import sleep +from os import path +from collections import namedtuple +from pyudev import DeviceNotFoundByNameError +from usrp_mpm import lib # Pulls in everything from C++-land +from usrp_mpm import tlv_eeprom +from usrp_mpm.cores import WhiteRabbitRegsControl +from usrp_mpm.components import ZynqComponents +from usrp_mpm.sys_utils import dtoverlay +from usrp_mpm.sys_utils import ectool +from usrp_mpm.sys_utils import i2c_dev +from usrp_mpm.sys_utils.gpio import Gpio +from usrp_mpm.sys_utils.udev import dt_symbol_get_spidev +from usrp_mpm.rpc_server import no_claim, no_rpc +from usrp_mpm.mpmutils import assert_compat_number, poll_with_timeout +from usrp_mpm.periph_manager import PeriphManagerBase +from usrp_mpm.xports import XportMgrUDP +from usrp_mpm.periph_manager.x4xx_periphs import MboardRegsControl +from usrp_mpm.periph_manager.x4xx_periphs import CtrlportRegs +from usrp_mpm.periph_manager.x4xx_periphs import DioControl +from usrp_mpm.periph_manager.x4xx_periphs import QSFPModule +from usrp_mpm.periph_manager.x4xx_periphs import get_temp_sensor +from usrp_mpm.periph_manager.x4xx_mb_cpld import MboardCPLD +from usrp_mpm.periph_manager.x4xx_clk_aux import ClockingAuxBrdControl +from usrp_mpm.periph_manager.x4xx_clk_mgr import X4xxClockMgr +from usrp_mpm.periph_manager.x4xx_gps_mgr import X4xxGPSMgr +from usrp_mpm.periph_manager.x4xx_rfdc_ctrl import X4xxRfdcCtrl +from usrp_mpm.dboard_manager.x4xx_db_iface import X4xxDboardIface +from usrp_mpm.dboard_manager.zbx import ZBX + + +X400_DEFAULT_EXT_CLOCK_FREQ = 10e6 +X400_DEFAULT_MASTER_CLOCK_RATE = 122.88e6 +X400_DEFAULT_TIME_SOURCE = X4xxClockMgr.TIME_SOURCE_INTERNAL +X400_DEFAULT_CLOCK_SOURCE = X4xxClockMgr.CLOCK_SOURCE_INTERNAL +X400_DEFAULT_ENABLE_PPS_EXPORT = True +X400_FPGA_COMPAT = (7, 2) +X400_DEFAULT_TRIG_DIRECTION = ClockingAuxBrdControl.DIRECTION_OUTPUT +X400_MONITOR_THREAD_INTERVAL = 1.0 # seconds +QSFPModuleConfig = namedtuple("QSFPModuleConfig", "modprs modsel devsymbol") +X400_QSFP_I2C_CONFIGS = [ + QSFPModuleConfig(modprs='QSFP0_MODPRS', modsel='QSFP0_MODSEL_n', devsymbol='qsfp0_i2c'), + QSFPModuleConfig(modprs='QSFP1_MODPRS', modsel='QSFP1_MODSEL_n', devsymbol='qsfp1_i2c')] +RPU_SUCCESS_REPORT = 'Success' +RPU_FAILURE_REPORT = 'Failure' +RPU_REMOTEPROC_FIRMWARE_PATH = '/lib/firmware' +RPU_REMOTEPROC_PREFIX_PATH = '/sys/class/remoteproc/remoteproc' +RPU_REMOTEPROC_PROPERTY_FIRMWARE = 'firmware' +RPU_REMOTEPROC_PROPERTY_STATE = 'state' +RPU_STATE_COMMAND_START = 'start' +RPU_STATE_COMMAND_STOP = 'stop' +RPU_STATE_OFFLINE = 'offline' +RPU_STATE_RUNNING = 'running' +RPU_MAX_FIRMWARE_SIZE = 0x100000 +RPU_MAX_STATE_CHANGE_TIME_IN_MS = 10000 +RPU_STATE_CHANGE_POLLING_INTERVAL_IN_MS = 100 + +DIOAUX_EEPROM = "dioaux_eeprom" +DIOAUX_PID = 0x4003 + +# pylint: disable=too-few-public-methods +class EepromTagMap: + """ + Defines the tagmap for EEPROMs matching this magic. + The tagmap is a dictionary mapping an 8-bit tag to a NamedStruct instance. + The canonical list of tags and the binary layout of the associated structs + is defined in mpm/tools/tlv_eeprom/usrp_eeprom.h. Only the subset relevant + to MPM are included below. + """ + magic = 0x55535250 + tagmap = { + # 0x10: usrp_eeprom_board_info + 0x10: tlv_eeprom.NamedStruct('< H H H 7s 1x', + ['pid', 'rev', 'rev_compat', 'serial']), + # 0x11: usrp_eeprom_module_info + 0x11: tlv_eeprom.NamedStruct('< H H 7s 1x', + ['module_pid', 'module_rev', 'module_serial']), + } + + +############################################################################### +# Transport managers +############################################################################### +class X400XportMgrUDP(XportMgrUDP): + "X400-specific UDP configuration" + iface_config = { + 'sfp0': { + 'label': 'misc-enet-regs0', + 'type': 'sfp', + }, + 'sfp0_1': { + 'label': 'misc-enet-regs0-1', + 'type': 'sfp', + }, + 'sfp0_2': { + 'label': 'misc-enet-regs0-2', + 'type': 'sfp', + }, + 'sfp0_3': { + 'label': 'misc-enet-regs0-3', + 'type': 'sfp', + }, + 'sfp1': { + 'label': 'misc-enet-regs1', + 'type': 'sfp', + }, + 'sfp1_1': { + 'label': 'misc-enet-regs1-1', + 'type': 'sfp', + }, + 'sfp1_2': { + 'label': 'misc-enet-regs1-2', + 'type': 'sfp', + }, + 'sfp1_3': { + 'label': 'misc-enet-regs1-3', + 'type': 'sfp', + }, + 'int0': { + 'label': 'misc-enet-int-regs', + 'type': 'internal', + }, + 'eth0': { + 'label': '', + 'type': 'forward', + } + } +# pylint: enable=too-few-public-methods + + +############################################################################### +# Main Class +############################################################################### +class x4xx(ZynqComponents, PeriphManagerBase): + """ + Holds X400 specific attributes and methods + """ + ######################################################################### + # Overridables + # + # See PeriphManagerBase for documentation on these fields. We try and keep + # them in the same order as they are in PeriphManagerBase for easier lookup. + ######################################################################### + pids = {0x0410: 'x410'} + description = "X400-Series Device" + eeprom_search = PeriphManagerBase._EepromSearch.SYMBOL + # This is not in the overridables section from PeriphManagerBase, but we use + # it below + eeprom_magic = EepromTagMap.magic + mboard_eeprom_offset = 0 + mboard_eeprom_max_len = 256 + mboard_eeprom_magic = eeprom_magic + mboard_info = {"type": "x4xx"} + mboard_max_rev = 5 # RevE + max_num_dboards = 2 + mboard_sensor_callback_map = { + # List of motherboard sensors that are always available. There are also + # GPS sensors, but they get added during __init__() only when there is + # a GPS available. + 'ref_locked': 'get_ref_lock_sensor', + 'fan0': 'get_fan0_sensor', + 'fan1': 'get_fan1_sensor', + 'temp_fpga' : 'get_fpga_temp_sensor', + 'temp_internal' : 'get_internal_temp_sensor', + 'temp_main_power' : 'get_main_power_temp_sensor', + 'temp_scu_internal' : 'get_scu_internal_temp_sensor', + } + db_iface = X4xxDboardIface + dboard_eeprom_magic = eeprom_magic + updateable_components = { + 'fpga': { + 'callback': "update_fpga", + 'path': '/lib/firmware/{}.bin', + 'reset': True, + 'check_dts_for_compatibility': True, + 'compatibility': { + 'fpga': { + 'current': X400_FPGA_COMPAT, + 'oldest': (7, 0), + }, + 'cpld_ifc' : { + 'current': (2, 0), + 'oldest': (2, 0), + }, + 'db_gpio_ifc': { + 'current': (1, 0), + 'oldest': (1, 0), + }, + 'rf_core_100m': { + 'current': (1, 0), + 'oldest': (1, 0), + }, + 'rf_core_400m': { + 'current': (1, 0), + 'oldest': (1, 0), + }, + } + }, + 'dts': { + 'callback': "update_dts", + 'path': '/lib/firmware/{}.dts', + 'output': '/lib/firmware/{}.dtbo', + 'reset': False, + }, + } + discoverable_features = ["ref_clk_calibration", "time_export"] + # + # End of overridables from PeriphManagerBase + ########################################################################### + + + # X400-specific settings + # Label for the mboard UIO + mboard_regs_label = "mboard-regs" + ctrlport_regs_label = "ctrlport-mboard-regs" + # Label for the white rabbit UIO + wr_regs_label = "wr-regs" + # Override the list of updateable components + # X4xx specific discoverable features + + @classmethod + def generate_device_info(cls, eeprom_md, mboard_info, dboard_infos): + """ + Hard-code our product map + """ + # Add the default PeriphManagerBase information first + device_info = super().generate_device_info( + eeprom_md, mboard_info, dboard_infos) + # Then add X4xx-specific information + mb_pid = eeprom_md.get('pid') + device_info['product'] = cls.pids.get(mb_pid, 'unknown') + module_serial = eeprom_md.get('module_serial') + if module_serial is not None: + device_info['serial'] = module_serial + return device_info + + @staticmethod + def list_required_dt_overlays(device_info): + """ + Lists device tree overlays that need to be applied before this class can + be used. List of strings. + Are applied in order. + + eeprom_md -- Dictionary of info read out from the mboard EEPROM + device_args -- Arbitrary dictionary of info, typically user-defined + """ + return [device_info['product']] + + def _init_mboard_overlays(self): + """ + Load all required overlays for this motherboard + Overriden from the base implementation to force apply even if + the overlay was already loaded. + """ + requested_overlays = self.list_required_dt_overlays( + self.device_info, + ) + self.log.debug("Motherboard requests device tree overlays: {}".format( + requested_overlays + )) + # Remove all overlays before applying new ones + for overlay in requested_overlays: + dtoverlay.rm_overlay_safe(overlay) + for overlay in requested_overlays: + dtoverlay.apply_overlay_safe(overlay) + # Need to wait here a second to make sure the ethernet interfaces are up + # TODO: Fine-tune this number, or wait for some smarter signal. + sleep(1) + + ########################################################################### + # Ctor and device initialization tasks + ########################################################################### + def __init__(self, args): + super(x4xx, self).__init__() + + self._tear_down = False + self._rpu_initialized = False + self._status_monitor_thread = None + self._master_clock_rate = None + self._gps_mgr = None + self._clk_mgr = None + self._safe_sync_source = { + 'clock_source': X400_DEFAULT_CLOCK_SOURCE, + 'time_source': X400_DEFAULT_TIME_SOURCE, + } + self.rfdc = None + self.mboard_regs_control = None + self.ctrlport_regs = None + self.cpld_control = None + self.dio_control = None + try: + self._init_peripherals(args) + self.init_dboards(args) + self._clk_mgr.set_dboard_reset_cb( + lambda enable: [db.reset_clock(enable) for db in self.dboards]) + except Exception as ex: + self.log.error("Failed to initialize motherboard: %s", str(ex), exc_info=ex) + self._initialization_status = str(ex) + self._device_initialized = False + if not self._device_initialized: + # Don't try and figure out what's going on. Just give up. + return + try: + if not args.get('skip_boot_init', False): + self.init(args) + except Exception as ex: + self.log.warning("Failed to initialize device on boot: %s", str(ex)) + + # The parent class versions of these functions require access to self, but + # these versions don't. + # pylint: disable=no-self-use + def _read_mboard_eeprom_data(self, eeprom_path): + """ Returns a tuple (eeprom_dict, eeprom_rawdata) for the motherboard + EEPROM. + """ + return tlv_eeprom.read_eeprom(eeprom_path, EepromTagMap.tagmap, + EepromTagMap.magic, None) + + def _read_dboard_eeprom_data(self, eeprom_path): + """ Returns a tuple (eeprom_dict, eeprom_rawdata) for a daughterboard + EEPROM. + """ + return tlv_eeprom.read_eeprom(eeprom_path, EepromTagMap.tagmap, + EepromTagMap.magic, None) + # pylint: enable=no-self-use + + def _check_fpga_compat(self): + " Throw an exception if the compat numbers don't match up " + actual_compat = self.mboard_regs_control.get_compat_number() + self.log.debug("Actual FPGA compat number: {:d}.{:d}".format( + actual_compat[0], actual_compat[1] + )) + assert_compat_number( + X400_FPGA_COMPAT, + actual_compat, + component="FPGA", + fail_on_old_minor=False, + log=self.log + ) + + def _init_gps_mgr(self): + """ + Initialize the GPS manager and the sensors. + Note that mpmd_impl queries all available sensors at initialization + time, in order to populate the property tree. That means we can't + dynamically load/unload sensors. Instead, we have to make sure that + the sensors can handle the GPS sensors, even when it's disabled. That + is pushed into the GPS manager class. + """ + self.log.debug("Found GPS, adding sensors.") + gps_mgr = X4xxGPSMgr(self._clocking_auxbrd, self.log) + # We can't use _add_public_methods(), because we only want a subset of + # the public methods. Also, we want to know which sensors were added so + # we can also add them to mboard_sensor_callback_map. + new_methods = gps_mgr.extend(self) + self.mboard_sensor_callback_map.update(new_methods) + return gps_mgr + + def _monitor_status(self): + """ + Status monitoring thread: This should be executed in a thread. It will + continuously monitor status of the following peripherals: + + - REF lock (update back-panel REF LED) + """ + self.log.trace("Launching monitor loop...") + cond = threading.Condition() + cond.acquire() + while not self._tear_down: + ref_locked = self.get_ref_lock_sensor()['value'] == 'true' + if self._clocking_auxbrd is not None: + self._clocking_auxbrd.set_ref_lock_led(ref_locked) + # Now wait + if cond.wait_for( + lambda: self._tear_down, + X400_MONITOR_THREAD_INTERVAL): + break + cond.release() + self.log.trace("Terminating monitor loop.") + + def _assert_rfdc_powered(self): + """ + Assert that RFdc power is enabled, throw RuntimeError otherwise. + """ + if not self._rfdc_powered.get(): + err_msg = "RFDC is not powered on" + self.log.error(err_msg) + raise RuntimeError(err_msg) + + def _get_serial_number(self): + """ + Read the serial number from eeprom, falling back to the board S/N + if the module S/N is not populated. + """ + serial_number = self._eeprom_head.get("module_serial") + if serial_number is None: + self.log.warning( + 'Module serial number not programmed, falling back to motherboard serial') + serial_number = self._eeprom_head["serial"] + return serial_number.rstrip(b'\x00') + + def _init_peripherals(self, args): + """ + Turn on all peripherals. This may throw an error on failure, so make + sure to catch it. + """ + # Sanity checks + assert self.mboard_info.get('product') in self.pids.values(), \ + "Device product could not be determined!" + # Init peripherals + self._rfdc_powered = Gpio('RFDC_POWERED', Gpio.INPUT) + # Init RPU Manager + self.log.trace("Initializing RPU manager peripheral...") + self.init_rpu() + # Init clocking aux board + self.log.trace("Initializing Clocking Aux Board controls...") + has_gps = False + try: + self._clocking_auxbrd = ClockingAuxBrdControl() + self.log.trace("Initialized Clocking Aux Board controls") + has_gps = self._clocking_auxbrd.is_gps_supported() + except RuntimeError: + self.log.warning( + "GPIO I2C bus could not be found for the Clocking Aux Board, " + "disabling Clocking Aux Board functionality.") + self._clocking_auxbrd = None + self._safe_sync_source = { + 'clock_source': X4xxClockMgr.CLOCK_SOURCE_MBOARD, + 'time_source': X4xxClockMgr.TIME_SOURCE_INTERNAL, + } + + initial_clock_source = args.get('clock_source', X400_DEFAULT_CLOCK_SOURCE) + if self._clocking_auxbrd: + self._add_public_methods(self._clocking_auxbrd, "clkaux") + else: + initial_clock_source = X4xxClockMgr.CLOCK_SOURCE_MBOARD + + # Init CPLD before talking to clocking ICs + cpld_spi_node = dt_symbol_get_spidev('mb_cpld') + self.cpld_control = MboardCPLD(cpld_spi_node, self.log) + self.cpld_control.check_signature() + self.cpld_control.check_compat_version() + self.cpld_control.trace_git_hash() + + self._assert_rfdc_powered() + # Init clocking after CPLD as the SPLL communication is relying on it. + # We try and guess the correct master clock rate here based on defaults + # and args. Since we are still in __init__(), the args that come from mpm.conf + # are empty. We can't detect the real default MCR, because we need + # the RFDC controls for that -- but they won't work without clocks. So + # let's pick a sensible default MCR value, init the clocks, and fix the + # MCR value further down. + self._master_clock_rate = float( + args.get('master_clock_rate', X400_DEFAULT_MASTER_CLOCK_RATE)) + sample_clock_freq, _, is_legacy_mode, _ = \ + X4xxRfdcCtrl.master_to_sample_clk[self._master_clock_rate] + self._clk_mgr = X4xxClockMgr( + initial_clock_source, + time_source=args.get('time_source', X400_DEFAULT_TIME_SOURCE), + ref_clock_freq=float(args.get( + 'ext_clock_freq', X400_DEFAULT_EXT_CLOCK_FREQ)), + sample_clock_freq=sample_clock_freq, + is_legacy_mode=is_legacy_mode, + clk_aux_board=self._clocking_auxbrd, + cpld_control=self.cpld_control, + log=self.log) + self._add_public_methods( + self._clk_mgr, + prefix="", + filter_cb=lambda name, method: not hasattr(method, '_norpc') + ) + + # Overlay must be applied after clocks have been configured + self.overlay_apply() + + # Init Mboard Regs + self.log.trace("Initializing MBoard reg controls...") + serial_number = self._get_serial_number() + self.mboard_regs_control = MboardRegsControl( + self.mboard_regs_label, self.log) + self._check_fpga_compat() + self.mboard_regs_control.set_serial_number(serial_number) + self.mboard_regs_control.get_git_hash() + self.mboard_regs_control.get_build_timestamp() + self._clk_mgr.mboard_regs_control = self.mboard_regs_control + + # Create control for RFDC + self.rfdc = X4xxRfdcCtrl(self._clk_mgr.get_spll_freq, self.log) + self._add_public_methods( + self.rfdc, prefix="", + filter_cb=lambda name, method: not hasattr(method, '_norpc') + ) + + self._update_fpga_type() + + # Force reset the RFDC to ensure it is in a good state + self.rfdc.set_reset(reset=True) + self.rfdc.set_reset(reset=False) + + # Synchronize SYSREF and clock distributed to all converters + self.rfdc.sync() + self._clk_mgr.set_rfdc_reset_cb(self.rfdc.set_reset) + + # The initial default mcr only works if we have an FPGA with + # a decimation of 2. But we need the overlay applied before we + # can detect decimation, and that requires clocks to be initialized. + self.set_master_clock_rate(self.rfdc.get_default_mcr()) + + # Init ctrlport endpoint + self.ctrlport_regs = CtrlportRegs(self.ctrlport_regs_label, self.log) + + # Init IPass cable status forwarding and CMI + self.cpld_control.set_serial_number(serial_number) + self.cpld_control.set_cmi_device_ready( + self.mboard_regs_control.is_pcie_present()) + # The CMI transmission can be disabled by setting the cable status + # to be not connected. All images except for the LV PCIe variant + # provide a fixed "cables are unconnected" status. The LV PCIe image + # reports the correct status. As the FPGA holds this information it + # is possible to always enable the iPass cable present forwarding. + self.ctrlport_regs.enable_cable_present_forwarding(True) + + # Init DIO + if self._check_compat_aux_board(DIOAUX_EEPROM, DIOAUX_PID): + self.dio_control = DioControl(self.mboard_regs_control, + self.cpld_control, self.log) + # add dio_control public methods to MPM API + self._add_public_methods(self.dio_control, "dio") + + # Init QSFP modules + for idx, config in enumerate(X400_QSFP_I2C_CONFIGS): + attr = QSFPModule( + config.modprs, config.modsel, config.devsymbol, self.log) + setattr(self, "_qsfp_module{}".format(idx), attr) + self._add_public_methods(attr, "qsfp{}".format(idx)) + + # Init GPS + if has_gps: + self._gps_mgr = self._init_gps_mgr() + # Init CHDR transports + self._xport_mgrs = { + 'udp': X400XportMgrUDP(self.log, args), + } + # Spawn status monitoring thread + self.log.trace("Spawning status monitor thread...") + self._status_monitor_thread = threading.Thread( + target=self._monitor_status, + name="X4xxStatusMonitorThread", + daemon=True, + ) + self._status_monitor_thread.start() + # Init complete. + self.log.debug("Device info: {}".format(self.device_info)) + + def _check_compat_aux_board(self, name, pid): + """ + Check whether auxiliary board given by name and pid can be found + :param name: symbol name of the auxiliary board which is used as + lookup for the dictionary of available boards. + :param pid: PID the board must have to be considered compatible + :return True if board is available with matching PID, + False otherwise + """ + assert(isinstance(self._aux_board_infos, dict)), "No EEPROM data" + board_info = self._aux_board_infos.get(name, None) + if board_info is None: + self.log.warning("Board for %s not present" % name) + return False + if board_info.get("pid", 0) != pid: + self.log.error("Expected PID for board %s to be 0x%04x but found " + "0x%04x" % (name, pid, board_info["pid"])) + return False + self.log.debug("Found compatible board for %s " + "(PID: 0x%04x)" % (name, board_info["pid"])) + return True + + def init_rpu(self): + """ + Initializes the RPU image manager + """ + if self._rpu_initialized: + return + + # Check presence/state of RPU cores + try: + for core_number in [0, 1]: + self.log.trace( + "RPU Core %d state: %s", + core_number, + self.get_rpu_state(core_number)) + # TODO [psisterh, 5 Dec 2019] + # Should we force core to + # stop if running or in error state? + self.log.trace("Initialized RPU cores successfully.") + self._rpu_initialized = True + except FileNotFoundError: + self.log.warning( + "Failed to initialize RPU: remoteproc sysfs not present.") + + ########################################################################### + # Session init and deinit + ########################################################################### + def init(self, args): + """ + Calls init() on the parent class, and then programs the Ethernet + dispatchers accordingly. + """ + if not self._device_initialized: + self.log.warning( + "Cannot run init(), device was never fully initialized!") + return False + + # We need to disable the PPS out during clock and dboard initialization in order + # to avoid glitches. + if self._clocking_auxbrd is not None: + self._clocking_auxbrd.set_trig(False) + + # If the caller has not specified clock_source or time_source, set them + # to the values currently configured. + args['clock_source'] = args.get('clock_source', self._clk_mgr.get_clock_source()) + args['time_source'] = args.get('time_source', self._clk_mgr.get_time_source()) + self.set_sync_source(args) + + # If a Master Clock Rate was specified, + # re-configure the Sample PLL and all downstream clocks + if 'master_clock_rate' in args: + self.set_master_clock_rate(float(args['master_clock_rate'])) + + # Initialize CtrlportRegs (manually opens the UIO resource for faster access) + self.ctrlport_regs.init() + + # Note: The parent class takes care of calling init() on all the + # daughterboards + result = super(x4xx, self).init(args) + + # Now the clocks are all enabled, we can also enable PPS export: + if self._clocking_auxbrd is not None: + self._clocking_auxbrd.set_trig( + args.get('pps_export', X400_DEFAULT_ENABLE_PPS_EXPORT), + args.get('trig_direction', X400_DEFAULT_TRIG_DIRECTION) + ) + + for xport_mgr in self._xport_mgrs.values(): + xport_mgr.init(args) + return result + + def deinit(self): + """ + Clean up after a UHD session terminates. + """ + if not self._device_initialized: + self.log.warning( + "Cannot run deinit(), device was never fully initialized!") + return + + if self.get_ref_lock_sensor()['unit'] != 'locked': + self.log.error("ref clocks aren't locked, falling back to default") + source = {"clock_source": X400_DEFAULT_CLOCK_SOURCE, + "time_source": X400_DEFAULT_TIME_SOURCE + } + self.set_sync_source(source) + super(x4xx, self).deinit() + self.ctrlport_regs.deinit() + for xport_mgr in self._xport_mgrs.values(): + xport_mgr.deinit() + + def tear_down(self): + """ + Tear down all members that need to be specially handled before + deconstruction. + For X400, this means the overlay. + """ + self.log.trace("Tearing down X4xx device...") + self._tear_down = True + if self._device_initialized: + self._status_monitor_thread.join(3 * X400_MONITOR_THREAD_INTERVAL) + if self._status_monitor_thread.is_alive(): + self.log.error("Could not terminate monitor thread! " + "This could result in resource leaks.") + # call tear_down on daughterboards first + super(x4xx, self).tear_down() + if self.dio_control is not None: + self.dio_control.tear_down() + self.rfdc.unset_cbs() + self._clk_mgr.unset_cbs() + # remove x4xx overlay + active_overlays = self.list_active_overlays() + self.log.trace("X4xx has active device tree overlays: {}".format( + active_overlays + )) + for overlay in active_overlays: + dtoverlay.rm_overlay(overlay) + + ########################################################################### + # Transport API + ########################################################################### + # pylint: disable=no-self-use + def get_chdr_link_types(self): + """ + This will only ever return a single item (udp). + """ + return ["udp"] + # pylint: enable=no-self-use + + def get_chdr_link_options(self, xport_type): + """ + Returns a list of dictionaries. Every dictionary contains information + about one way to connect to this device in order to initiate CHDR + traffic. + + The interpretation of the return value is very highly dependant on the + transport type (xport_type). + For UDP, the every entry of the list has the following keys: + - ipv4 (IP Address) + - port (UDP port) + - link_rate (bps of the link, e.g. 10e9 for 10GigE) + """ + if xport_type not in self._xport_mgrs: + self.log.warning("Can't get link options for unknown link type: `{}'.") + return [] + if xport_type == "udp": + return self._xport_mgrs[xport_type].get_chdr_link_options( + self.mboard_info['rpc_connection']) + # else: + return self._xport_mgrs[xport_type].get_chdr_link_options() + + ########################################################################### + # Device info + ########################################################################### + def get_device_info_dyn(self): + """ + Append the device info with current IP addresses. + """ + if not self._device_initialized: + return {} + device_info = self._xport_mgrs['udp'].get_xport_info() + device_info.update({ + 'fpga_version': "{}.{}".format( + *self.mboard_regs_control.get_compat_number()), + 'fpga_version_hash': "{:x}.{}".format( + *self.mboard_regs_control.get_git_hash()), + 'fpga': self.updateable_components.get('fpga', {}).get('type', ""), + }) + return device_info + + def is_db_gpio_ifc_present(self, slot_id): + """ + Return if daughterboard GPIO interface at 'slot_id' is present in the FPGA + """ + db_gpio_version = self.mboard_regs_control.get_db_gpio_ifc_version(slot_id) + return db_gpio_version[0] > 0 + + ########################################################################### + # Clock/Time API + ########################################################################### + def get_clock_sources(self): + """ + Lists all available clock sources. + """ + return self._clk_mgr.get_clock_sources() + + def set_clock_source(self, *args): + """ + Ensures the new reference clock source and current time source pairing + is valid and sets both by calling set_sync_source(). + """ + clock_source = args[0] + time_source = self._clk_mgr.get_time_source() + assert clock_source is not None + assert time_source is not None + if (clock_source, time_source) not in self._clk_mgr.valid_sync_sources: + old_time_source = time_source + if clock_source in ( + X4xxClockMgr.CLOCK_SOURCE_MBOARD, + X4xxClockMgr.CLOCK_SOURCE_INTERNAL): + time_source = X4xxClockMgr.TIME_SOURCE_INTERNAL + elif clock_source == X4xxClockMgr.CLOCK_SOURCE_EXTERNAL: + time_source = X4xxClockMgr.TIME_SOURCE_EXTERNAL + elif clock_source == X4xxClockMgr.CLOCK_SOURCE_GPSDO: + time_source = X4xxClockMgr.TIME_SOURCE_GPSDO + self.log.warning( + f"Time source '{old_time_source}' is an invalid selection with " + f"clock source '{clock_source}'. " + f"Coercing time source to '{time_source}'") + self.set_sync_source({ + "clock_source": clock_source, "time_source": time_source}) + + def set_clock_source_out(self, enable): + """ + Allows routing the clock configured as source on the clk aux board to + the RefOut terminal. This only applies to internal, gpsdo and nsync. + """ + self._clk_mgr.set_clock_source_out(enable) + + def get_time_sources(self): + " Returns list of valid time sources " + return self._clk_mgr.get_time_sources() + + def set_time_source(self, time_source): + """ + Set a time source + + This will call set_sync_source() internally, and use the current clock + source as a clock source. If the current clock source plus the requested + time source is not a valid combination, it will coerce the clock source + to a valid choice and print a warning. + """ + clock_source = self._clk_mgr.get_clock_source() + assert clock_source is not None + assert time_source is not None + if (clock_source, time_source) not in self._clk_mgr.valid_sync_sources: + old_clock_source = clock_source + if time_source == X4xxClockMgr.TIME_SOURCE_QSFP0: + clock_source = X4xxClockMgr.CLOCK_SOURCE_MBOARD + elif time_source == X4xxClockMgr.TIME_SOURCE_INTERNAL: + clock_source = X4xxClockMgr.CLOCK_SOURCE_MBOARD + elif time_source == X4xxClockMgr.TIME_SOURCE_EXTERNAL: + clock_source = X4xxClockMgr.CLOCK_SOURCE_EXTERNAL + elif time_source == X4xxClockMgr.TIME_SOURCE_GPSDO: + clock_source = X4xxClockMgr.CLOCK_SOURCE_GPSDO + self.log.warning( + 'Clock source {} is an invalid selection with time source {}. ' + 'Coercing clock source to {}' + .format(old_clock_source, time_source, clock_source)) + self.set_sync_source( + {"time_source": time_source, "clock_source": clock_source}) + + def set_sync_source(self, args): + """ + Selects reference clock and PPS sources. Unconditionally re-applies the + time source to ensure continuity between the reference clock and time + rates. + Note that if we change the source such that the time source is changed + to 'external', then we need to also disable exporting the reference + clock (RefOut and PPS-In are the same SMA connector). + """ + # Check the clock source, time source, and combined pair are valid: + clock_source = args.get('clock_source', self._clk_mgr.get_clock_source()) + if clock_source not in self.get_clock_sources(): + raise ValueError(f'Clock source {clock_source} is not a valid selection') + time_source = args.get('time_source', self._clk_mgr.get_time_source()) + if time_source not in self.get_time_sources(): + raise ValueError(f'Time source {time_source} is not a valid selection') + if (clock_source, time_source) not in self._clk_mgr.valid_sync_sources: + raise ValueError( + f'Clock and time source pair ({clock_source}, {time_source}) is ' + 'not a valid selection') + # Sanity checks complete. Now check if we need to disable the RefOut. + # Reminder: RefOut and PPSIn share an SMA. Besides, you can't export an + # external clock. We are thus not checking for time_source == 'external' + # because that's a subset of clock_source == 'external'. + # We also disable clock exports for 'mboard', because the mboard clock + # does not get routed back to the clocking aux board and thus can't be + # exported either. + if clock_source in (X4xxClockMgr.CLOCK_SOURCE_EXTERNAL, + X4xxClockMgr.CLOCK_SOURCE_MBOARD) and \ + self._clocking_auxbrd: + self._clocking_auxbrd.export_clock(enable=False) + # Now the clock manager can do its thing. + ret_val = self._clk_mgr.set_sync_source(clock_source, time_source) + if ret_val == self._clk_mgr.SetSyncRetVal.NOP: + return + try: + # Re-set master clock rate. If this doesn't work, it will time out + # and throw an exception. We need to put the device back into a safe + # state in that case. + self.set_master_clock_rate(self._master_clock_rate) + except RuntimeError as ex: + err = f"Setting clock_source={clock_source},time_source={time_source} " \ + f"failed, falling back to {self._safe_sync_source}. Error: " \ + f"{ex}" + self.log.error(err) + if args.get('__noretry__', False): + self.log.error("Giving up.") + else: + self.set_sync_source({**self._safe_sync_source, '__noretry__': True}) + raise + + def set_master_clock_rate(self, master_clock_rate): + """ + Sets the master clock rate by configuring the RFDC decimation and SPLL, + and then resetting downstream clocks. + """ + if master_clock_rate not in self.rfdc.master_to_sample_clk: + self.log.error('Unsupported master clock rate selection {}' + .format(master_clock_rate)) + raise RuntimeError('Unsupported master clock rate selection') + sample_clock_freq, decimation, is_legacy_mode, halfband = \ + self.rfdc.master_to_sample_clk[master_clock_rate] + for db_idx, _ in enumerate(self.dboards): + db_rfdc_resamp, db_halfband = self.rfdc.get_rfdc_resampling_factor(db_idx) + if db_rfdc_resamp != decimation or db_halfband != halfband: + msg = (f'master_clock_rate {master_clock_rate} is not compatible ' + f'with FPGA which expected decimation {db_rfdc_resamp}') + self.log.error(msg) + raise RuntimeError(msg) + self.log.trace(f"Set master clock rate (SPLL) to: {master_clock_rate}") + self._clk_mgr.set_spll_rate(sample_clock_freq, is_legacy_mode) + self._master_clock_rate = master_clock_rate + self.rfdc.sync() + self._clk_mgr.config_pps_to_timekeeper(master_clock_rate) + + def set_trigger_io(self, direction): + """ + Switch direction of clocking board Trigger I/O SMA socket. + IMPORTANT! Ensure downstream devices depending on TRIG I/O's output ignore + this signal when calling this method or re-run their synchronization routine + after calling this method. The output-enable control is async. to the output. + :param self: + :param direction: "off" trigger io socket unused + "pps_output" device will output PPS signal + "input" PPS is fed into the device from external + :return: success status as boolean + """ + OFF = "off" + INPUT = "input" + PPS_OUTPUT = "pps_output" + directions = [OFF, INPUT, PPS_OUTPUT] + + if not self._clocking_auxbrd: + raise RuntimeError("No clocking aux board available") + if not direction in directions: + raise RuntimeError("Invalid trigger io direction (%s). Use one of %s" + % (direction, directions)) + + # Switching order of trigger I/O lines depends on requested direction. + # Always turn on new driver last so both drivers cannot be active + # simultaneously. + if direction == INPUT: + self.mboard_regs_control.set_trig_io_output(False) + self._clocking_auxbrd.set_trig(1, ClockingAuxBrdControl.DIRECTION_INPUT) + elif direction == PPS_OUTPUT: + self._clocking_auxbrd.set_trig(1, ClockingAuxBrdControl.DIRECTION_OUTPUT) + self.mboard_regs_control.set_trig_io_output(True) + else: # direction == OFF: + self.mboard_regs_control.set_trig_io_output(False) + self._clocking_auxbrd.set_trig(0) + + return True + + ########################################################################### + # EEPROMs + ########################################################################### + def get_db_eeprom(self, dboard_idx): + """ + See PeriphManagerBase.get_db_eeprom() for docs. + """ + try: + dboard = self.dboards[dboard_idx] + except IndexError: + error_msg = "Attempted to access invalid dboard index `{}' " \ + "in get_db_eeprom()!".format(dboard_idx) + self.log.error(error_msg) + raise RuntimeError(error_msg) + db_eeprom_data = copy.copy(dboard.device_info) + return db_eeprom_data + + ########################################################################### + # Component updating + ########################################################################### + # Note: Component updating functions defined by ZynqComponents + @no_rpc + def _update_fpga_type(self): + """Update the fpga type stored in the updateable components""" + fpga_string = "{}_{}".format( + self.mboard_regs_control.get_fpga_type(), + self.rfdc.get_dsp_bw()) + self.log.debug("Updating mboard FPGA type info to {}".format(fpga_string)) + self.updateable_components['fpga']['type'] = fpga_string + + ####################################################################### + # Timekeeper API + ####################################################################### + def get_master_clock_rate(self): + """ Return the master clock rate set during init """ + return self._master_clock_rate + + def get_clocks(self): + """ + Gets the RFNoC-related clocks present in the FPGA design + """ + # TODO: The 200 and 40 MHz clocks should not be hard coded, and ideally + # be linked to the FPGA image somehow + return [ + { + 'name': 'radio_clk', + 'freq': str(self.get_master_clock_rate()), + 'mutable': 'true' + }, + { + 'name': 'bus_clk', + 'freq': str(200e6), + }, + { + 'name': 'ctrl_clk', + 'freq': str(40e6), + } + ] + + + ########################################################################### + # Utility for validating RPU core number + ########################################################################### + @no_rpc + def _validate_rpu_core_number(self, core_number): + if ((core_number < 0) or (core_number > 1)): + raise RuntimeError("RPU core number must be 0 or 1.") + + + ########################################################################### + # Utility for validating RPU state change command + ########################################################################### + @no_rpc + def _validate_rpu_state(self, new_state_command, previous_state): + if ((new_state_command != RPU_STATE_COMMAND_START) + and (new_state_command != RPU_STATE_COMMAND_STOP)): + raise RuntimeError("RPU state command must be start or stop.") + if ((new_state_command == RPU_STATE_COMMAND_START) + and (previous_state == RPU_STATE_RUNNING)): + raise RuntimeError("RPU already running.") + if ((new_state_command == RPU_STATE_COMMAND_STOP) + and (previous_state == RPU_STATE_OFFLINE)): + raise RuntimeError("RPU already offline.") + + ########################################################################### + # Utility for validating RPU firmware + ########################################################################### + @no_rpc + def _validate_rpu_firmware(self, firmware): + file_path = path.join(RPU_REMOTEPROC_FIRMWARE_PATH, firmware) + if not path.isfile(file_path): + raise RuntimeError("Specified firmware does not exist.") + + ########################################################################### + # Utility for reading contents of a file + ########################################################################### + @no_rpc + def _read_file(self, file_path): + self.log.trace("_read_file: file_path= %s", file_path) + with open(file_path, 'r') as f: + return f.read().strip() + + + ########################################################################### + # Utility for writing contents of a file + ########################################################################### + @no_rpc + def _write_file(self, file_path, data): + self.log.trace("_write_file: file_path= %s, data= %s", file_path, data) + with open(file_path, 'w') as f: + f.write(data) + + + ########################################################################### + # RPU Image Deployment API + ########################################################################### + def get_rpu_state(self, core_number, validate=True): + """ Report the state for the specified RPU core """ + if validate: + self._validate_rpu_core_number(core_number) + return self._read_file( + path.join( + RPU_REMOTEPROC_PREFIX_PATH + str(core_number), + RPU_REMOTEPROC_PROPERTY_STATE)) + + + def set_rpu_state(self, core_number, new_state_command, validate=True): + """ Set the specified state for the specified RPU core """ + if not self._rpu_initialized: + self.log.warning( + "Failed to set RPU state: RPU peripheral not "\ + "initialized.") + return RPU_FAILURE_REPORT + # OK, RPU is initialized, now go set its state: + if validate: + self._validate_rpu_core_number(core_number) + previous_state = self.get_rpu_state(core_number, False) + if validate: + self._validate_rpu_state(new_state_command, previous_state) + self._write_file( + path.join( + RPU_REMOTEPROC_PREFIX_PATH + str(core_number), + RPU_REMOTEPROC_PROPERTY_STATE), + new_state_command) + + # Give RPU core time to change state (might load new fw) + poll_with_timeout( + lambda: previous_state != self.get_rpu_state(core_number, False), + RPU_MAX_STATE_CHANGE_TIME_IN_MS, + RPU_STATE_CHANGE_POLLING_INTERVAL_IN_MS) + + # Quick validation of new state + resulting_state = self.get_rpu_state(core_number, False) + if ((new_state_command == RPU_STATE_COMMAND_START) + and (resulting_state != RPU_STATE_RUNNING)): + raise RuntimeError('Unable to start specified RPU core.') + if ((new_state_command == RPU_STATE_COMMAND_STOP) + and (resulting_state != RPU_STATE_OFFLINE)): + raise RuntimeError('Unable to stop specified RPU core.') + return RPU_SUCCESS_REPORT + + def get_rpu_firmware(self, core_number): + """ Report the firmware for the specified RPU core """ + self._validate_rpu_core_number(core_number) + return self._read_file( + path.join( + RPU_REMOTEPROC_PREFIX_PATH + str(core_number), + RPU_REMOTEPROC_PROPERTY_FIRMWARE)) + + + def set_rpu_firmware(self, core_number, firmware, start=0): + """ Deploy the image at the specified path to the RPU """ + self.log.trace("set_rpu_firmware") + self.log.trace( + "image path: %s, core_number: %d, start?: %d", + firmware, + core_number, + start) + + if not self._rpu_initialized: + self.log.warning( + "Failed to deploy RPU image: "\ + "RPU peripheral not initialized.") + return RPU_FAILURE_REPORT + # RPU is initialized, now go set firmware: + self._validate_rpu_core_number(core_number) + self._validate_rpu_firmware(firmware) + + # Stop the core if necessary + if self.get_rpu_state(core_number, False) == RPU_STATE_RUNNING: + self.set_rpu_state(core_number, RPU_STATE_COMMAND_STOP, False) + + # Set the new firmware path + self._write_file( + path.join( + RPU_REMOTEPROC_PREFIX_PATH + str(core_number), + RPU_REMOTEPROC_PROPERTY_FIRMWARE), + firmware) + + # Start the core if requested + if start != 0: + self.set_rpu_state(core_number, RPU_STATE_COMMAND_START, False) + return RPU_SUCCESS_REPORT + + ####################################################################### + # Debugging + # Provides temporary methods for arbitrary hardware access while + # development for these components is ongoing. + ####################################################################### + def peek_ctrlport(self, addr): + """ Peek the MPM Endpoint to ctrlport registers on the FPGA """ + return '0x{:X}'.format(self.ctrlport_regs.peek32(addr)) + + def poke_ctrlport(self, addr, val): + """ Poke the MPM Endpoint to ctrlport registers on the FPGA """ + self.ctrlport_regs.poke32(addr, val) + + def peek_cpld(self, addr): + """ Peek the PS portion of the MB CPLD """ + return '0x{:X}'.format(self.cpld_control.peek32(addr)) + + def poke_cpld(self, addr, val): + """ Poke the PS portion of the MB CPLD """ + self.cpld_control.poke32(addr, val) + + def peek_db(self, db_id, addr): + """ Peek the DB CPLD, even if the DB is not discovered by MPM """ + assert db_id in (0, 1) + self.cpld_control.enable_daughterboard(db_id) + return '0x{:X}'.format( + self.ctrlport_regs.get_db_cpld_iface(db_id).peek32(addr)) + + def poke_db(self, db_id, addr, val): + """ Poke the DB CPLD, even if the DB is not discovered by MPM """ + assert db_id in (0, 1) + self.cpld_control.enable_daughterboard(db_id) + self.ctrlport_regs.get_db_cpld_iface(db_id).poke32(addr, val) + + def peek_clkaux(self, addr): + """Peek the ClkAux DB over SPI""" + return '0x{:X}'.format(self._clocking_auxbrd.peek8(addr)) + + def poke_clkaux(self, addr, val): + """Poke the ClkAux DB over SPI""" + self._clocking_auxbrd.poke8(addr, val) + + ########################################################################### + # Sensors + ########################################################################### + def get_ref_lock_sensor(self): + """ + Return main refclock lock status. This is the lock status of the + reference and sample PLLs. + """ + lock_status = self._clk_mgr.get_ref_locked() + return { + 'name': 'ref_locked', + 'type': 'BOOLEAN', + 'unit': 'locked' if lock_status else 'unlocked', + 'value': str(lock_status).lower(), + } + + def get_fpga_temp_sensor(self): + """ Get temperature sensor reading of the X4xx FPGA. """ + self.log.trace("Reading FPGA temperature.") + return get_temp_sensor(["RFSoC"], log=self.log) + + def get_main_power_temp_sensor(self): + """ + Get temperature sensor reading of PM-BUS devices which supply + 0.85V power supply to RFSoC. + """ + self.log.trace("Reading PMBus Power Supply Chip(s) temperature.") + return get_temp_sensor(["PMBUS-0", "PMBUS-1"], log=self.log) + + def get_scu_internal_temp_sensor(self): + """ Get temperature sensor reading of STM32 SCU's internal sensor. """ + self.log.trace("Reading SCU internal temperature.") + return get_temp_sensor(["EC Internal"], log=self.log) + + def get_internal_temp_sensor(self): + """ TODO: Determine how to interpret this function """ + self.log.warning("Reading internal temperature is not yet implemented.") + return { + 'name': 'temperature', + 'type': 'REALNUM', + 'unit': 'C', + 'value': '-1' + } + + def _get_fan_sensor(self, fan='fan0'): + """ Get fan speed. """ + self.log.trace("Reading {} speed sensor.".format(fan)) + fan_rpm = -1 + try: + fan_rpm_all = ectool.get_fan_rpm() + fan_rpm = fan_rpm_all[fan] + except Exception as ex: + self.log.warning( + "Error occurred when getting {} speed value: {} " + .format(fan, str(ex))) + return { + 'name': fan, + 'type': 'INTEGER', + 'unit': 'rpm', + 'value': str(fan_rpm) + } + + def get_fan0_sensor(self): + """ Get fan0 speed. """ + return self._get_fan_sensor('fan0') + + def get_fan1_sensor(self): + """ Get fan1 speed.""" + return self._get_fan_sensor('fan1') + + def get_gps_sensor_status(self): + """ + Get all status of GPS as sensor dict + """ + assert self._gps_mgr + self.log.trace("Reading all GPS status pins") + return f""" + {self.get_gps_lock_sensor()} + {self.get_gps_alarm_sensor()} + {self.get_gps_warmup_sensor()} + {self.get_gps_survey_sensor()} + {self.get_gps_phase_lock_sensor()} + """ diff --git a/mpm/python/usrp_mpm/periph_manager/x4xx_clk_aux.py b/mpm/python/usrp_mpm/periph_manager/x4xx_clk_aux.py new file mode 100644 index 000000000..fcd655d9d --- /dev/null +++ b/mpm/python/usrp_mpm/periph_manager/x4xx_clk_aux.py @@ -0,0 +1,682 @@ +# +# Copyright 2021 Ettus Research, a National Instruments Brand +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +""" +Drivers for the X410 clocking aux board. +""" + +import subprocess +from usrp_mpm import lib # Pulls in everything from C++-land +from usrp_mpm import tlv_eeprom +from usrp_mpm.mpmlog import get_logger +from usrp_mpm.sys_utils.gpio import Gpio +from usrp_mpm.sys_utils.udev import dt_symbol_get_spidev +from usrp_mpm.sys_utils.udev import get_eeprom_paths_by_symbol +from usrp_mpm.sys_utils import i2c_dev +from usrp_mpm.chips import LMK05318 + +X400_CLKAUX_DEFAULT_TUNING_WORD = 0x200 # 1.65V which would be a DAC value of 512 +X400_CLKAUX_DEFAULT_REVISION = 0x1 +X400_CLKAUX_I2C_LABEL = 'clkaux_i2c' +X400_CLKAUX_SPI_LABEL = 'clkaux_spi' +X400_CLKAUX_GPSDO_PID = 0x4004 +X400_CLKAUX_NOGPSDO_PID = 0x4005 + + +def _check_i2c_bus(): + """ + Assert that the I2C connection to the clocking board is available in the + device tree. + """ + i2c_bus = i2c_dev.dt_symbol_get_i2c_bus(X400_CLKAUX_I2C_LABEL) + if i2c_bus is None: + raise RuntimeError("ClockingAuxBrdControl I2C bus not found") + +def _check_spi_bus(): + """ + Assert that the SPI connection to the clocking board is available in the + device tree. + """ + spi_node = dt_symbol_get_spidev(X400_CLKAUX_SPI_LABEL) + if spi_node is None: + raise RuntimeError("ClockingAuxBrdControl SPI bus not found") + + +# pylint: disable=too-few-public-methods +class ClkAuxTagMap: + """ + x4xx main tagmap is in the main class. Only the subset relevant to the + clocking aux board is included below. + """ + magic = 0x55535250 + tagmap = { + # 0x23: usrp_eeprom_clkaux_tuning_word + 0x23: tlv_eeprom.NamedStruct('< H', + ['tuning_word']), + # 0x10: usrp_eeprom_board_info + 0x10: tlv_eeprom.NamedStruct('< H H H 7s 1x', + ['pid', 'rev', 'rev_compat', 'serial']), + } + +# We are encapsulating a complicated piece of hardware here, so let's live with +# the fact that there will be many things hanging off of it. +# pylint: disable=too-many-public-methods +# pylint: disable=too-many-instance-attributes +class ClockingAuxBrdControl: + """ + Control interface for the Clocking Aux Board over I2C and SPI + """ + SOURCE_INTERNAL = "internal" + SOURCE_EXTERNAL = "external" + SOURCE_GPSDO = "gpsdo" + SOURCE_NSYNC = "nsync" + + SOURCE_NSYNC_LMK_PRI_FABRIC_CLK = "fabric_clk" + SOURCE_NSYNC_LMK_PRI_GTY_RCV_CLK = "gty_rcv_clk" + + NSYNC_PRI_REF = "pri_ref" + NSYNC_SEC_REF = "sec_ref" + + DIRECTION_INPUT = "input" + DIRECTION_OUTPUT = "output" + + VALID_CLK_EXPORTS = (SOURCE_INTERNAL, SOURCE_GPSDO, SOURCE_NSYNC) + VALID_NSYNC_LMK_PRI_REF_SOURCES = (SOURCE_NSYNC_LMK_PRI_FABRIC_CLK, + SOURCE_NSYNC_LMK_PRI_GTY_RCV_CLK) + + def __init__(self, default_source=None, parent_log=None): + self.log = \ + parent_log.getChild(self.__class__.__name__) if parent_log is not None \ + else get_logger(self.__class__.__name__) + _check_i2c_bus() + self._revision = self._get_eeprom_field('rev', X400_CLKAUX_DEFAULT_REVISION) + self._pid = self._get_eeprom_field('pid', X400_CLKAUX_NOGPSDO_PID) + self._nsync_support = self._revision >= 2 + self._gps_support = self._pid == X400_CLKAUX_GPSDO_PID + default_source = default_source or ClockingAuxBrdControl.SOURCE_INTERNAL + + # Some GPIO lines are named differently in the overlays for rev 1 and + # rev 2 and some perform different functions in rev 1 and 2 even if + # named similarly. + + # GPIO common to rev 1 and 2 + self._3v3_power_good = Gpio("CLKAUX_3V3_CLK_PG", Gpio.INPUT) + self._pps_term = Gpio("CLKAUX_PPS_TERM", Gpio.OUTPUT, 1) + self._trig_oe_n = Gpio("CLKAUX_TRIG_OEn", Gpio.OUTPUT, 1) + self._trig_dir = Gpio("CLKAUX_TRIG_DIR", Gpio.OUTPUT, 0) + self._ref_lck_led = Gpio("CLKAUX_REF_LCK", Gpio.OUTPUT, 0) + + if self._revision == 1: + self._ref_clk_sel_usr = Gpio("CLKAUX_REF_CLK_SEL_USR", Gpio.OUTPUT, 1) + self._mbrefclk_bias = Gpio("CLKAUX_MBRefCLK_En", Gpio.OUTPUT, 0) + self._exportclk_bias = Gpio("CLKAUX_ExportClk_En", Gpio.OUTPUT, 0) + elif self._revision >= 2: + self._ref_clk_sel_usr = Gpio("CLKAUX_UsrRefSel", Gpio.OUTPUT, 0) + self._mbrefclk_bias = Gpio("CLKAUX_MBRefCLK_Bias", Gpio.OUTPUT, 0) + self._exportclk_bias = Gpio("CLKAUX_ExportClk_Bias", Gpio.OUTPUT, 0) + self._tcxo_en = Gpio("CLKAUX_TCXO_EN", Gpio.OUTPUT, 0) + self._exportclk_en = Gpio("CLKAUX_EXP_CLK_EN", Gpio.OUTPUT, 0) + self._nsync_refsel = Gpio("CLKAUX_NSYNC_REFSEL", Gpio.OUTPUT, 0) + self._nsync_power_ctrl = Gpio("CLKAUX_NSYNC_PDN", Gpio.OUTPUT, 0) + self._ref_clk_select_net = Gpio("CLKAUX_REF_CLK_SEL_NET", Gpio.OUTPUT, 0) + self._nsync_gpio0 = Gpio("CLKAUX_NSYNC_GPIO0", Gpio.INPUT) + self._nsync_status1 = Gpio("CLKAUX_NSYNC_STATUS1", Gpio.INPUT) + self._nsync_status0 = Gpio("CLKAUX_NSYNC_STATUS0", Gpio.INPUT) + self._fpga_clk_gty_fabric_sel = Gpio("CLKAUX_FPGA_CLK_SEL", Gpio.OUTPUT, 0) + self._lmk05318_bias = Gpio("CLKAUX_05318Ref_Bias", Gpio.OUTPUT, 0) + + if self._gps_support: + self._gps_phase_lock = Gpio("CLKAUX_GPS_PHASELOCK", Gpio.INPUT) + self._gps_warmup = Gpio("CLKAUX_GPS_WARMUP", Gpio.INPUT) + self._gps_survey = Gpio("CLKAUX_GPS_SURVEY", Gpio.INPUT) + self._gps_lock = Gpio("CLKAUX_GPS_LOCK", Gpio.INPUT) + self._gps_alarm = Gpio("CLKAUX_GPS_ALARM", Gpio.INPUT) + self._gps_rst_n = Gpio("CLKAUX_GPS_RSTn", Gpio.OUTPUT, 0) + if self._revision == 1: + self._gps_initsrv_n = Gpio("CLKAUX_GPS_INITSRVn", Gpio.OUTPUT, 1) + elif self._revision >= 2: + self._gps_initsrv_n = Gpio("CLKAUX_GPS_INITSURVn", Gpio.OUTPUT, 0) + + if self._revision >= 2: + _check_spi_bus() + # Create SPI interface to the LMK05318 registers + nsync_spi_node = dt_symbol_get_spidev(X400_CLKAUX_SPI_LABEL) + nsync_lmk_regs_iface = lib.spi.make_spidev_regs_iface( + nsync_spi_node, + 1000000, # Speed (Hz) + 0x0, # SPI mode + 8, # Addr shift + 0, # Data shift + 1<<23, # Read flag + 0, # Write flag + ) + self._nsync_pll = LMK05318(nsync_lmk_regs_iface, self.log) + + self.set_source(default_source) + self.set_trig(False, self.DIRECTION_OUTPUT) + self._init_dac() + + def _init_dac(self): + """ + Initializes i2c bus to communicate with the DAC and configures the + tuning word for both voltage outputs + """ + dac_i2c_bus = i2c_dev.dt_symbol_get_i2c_bus(X400_CLKAUX_I2C_LABEL) + self._dac_i2c_iface = lib.i2c.make_i2cdev( + dac_i2c_bus, + 0xC, # addr + False, # ten_bit_addr + 100 + ) + tuning_word = self._get_eeprom_field('tuning_word', X400_CLKAUX_DEFAULT_TUNING_WORD) + self.config_dac(tuning_word, 0) + self.config_dac(tuning_word, 1) + + def _get_eeprom_field(self, field_name, default_val): + """ + Return the value of the requested eeprom field. + """ + eeprom_paths = get_eeprom_paths_by_symbol("clkaux_eeprom") + path = eeprom_paths['clkaux_eeprom'] + val = default_val + try: + eeprom, _ = tlv_eeprom.read_eeprom( + path, ClkAuxTagMap.tagmap, ClkAuxTagMap.magic, None) + val = eeprom.get(field_name, default_val) + except TypeError as err: + self.log.warning(f"Error reading eeprom; will use defaults. ({err})") + return val + + def store_tuning_word(self, tuning_word): + """ Store the dac tuning word in the ID EEPROM""" + cmd = ["eeprom-update", + "clkaux", + "--clkaux_tuning_word", + str(tuning_word)] + try: + subprocess.call(cmd) + except subprocess.CalledProcessError as ex: + self.log.warning("Failed to write to clkaux EEPROM: %s", str(ex)) + + def config_dac(self, tuning_word, out_select): + """Configure tuning word on the the selected DAC output through i2c""" + high, low = divmod(tuning_word << 6, 0x100) + command = 0x38 if out_select else 0x31 + tx_cmd = [command, high, low] + # Send command to write tuning word and update output + self._dac_i2c_iface.transfer(tx_cmd, 0, True) + + def read_dac(self, out_select): + """Read the tuning word on the selected DAC output through i2c""" + command = 0x8 if out_select else 0x1 + rx_buffer = self._dac_i2c_iface.transfer([command], 2, True) + val = ((rx_buffer[0] << 8) | rx_buffer[1]) >> 6 + return val + + def is_nsync_supported(self): + """Return True if nsync clock source is supported by hardware""" + return self._nsync_support + + def _check_nsync_supported(self): + """ + Assert nsync clock source is supported by hardware or throw otherwise + """ + if not self.is_nsync_supported(): + raise RuntimeError("NSYNC related features are not supported!") + + def is_gps_supported(self): + """Return True if GPS clock source is supported by hardware""" + return self._gps_support + + def is_gps_enabled(self): + """ + Return True if the GPS is currently enabled (i.e., not held in reset). + """ + return bool(self._gps_rst_n.get()) + + def _assert_gps_supported(self): + """ Throw a RuntimeError if GPS is not supported on this board. """ + if not self.is_gps_supported(): + raise RuntimeError("GPS related features are not supported!") + + def _init_nsync_lmk(self): + """Initialize the LMK05318 network sync IC""" + self._check_nsync_supported() + self.set_nsync_lmk_power_en(1) + self._nsync_pll.soft_reset(True) + self._nsync_pll.soft_reset(False) + if not self._nsync_pll.is_chip_id_valid(): + raise RuntimeError("ClockingAuxBrdControl Unable to locate LMK05318!") + + def set_source(self, clock_source, time_source=None): + """ + Select the clock and time source on the clock auxbrd. + + Notes: + - The clocking aux board has a PPS termination which must be disabled + when the PPS input is active. Note that we can only have an external + time source when the clock source is also external. We enable it in + all other cases. + - The pin that selects the reference clock (UsrRefSel or ref_clk_sel_usr) + selects *both* the clock reference and the time reference (external + or GPSDO). So if clock source is set to external, but time source to + internal, then we are still connecting the PPS In SMA to the FPGA. + - This function will disable clock export if we switch to external clock. + - The time source is irrelevant unless the clock source is EXTERNAL, so + we allow not specifying it for the other cases. + - We actually put the GPS chip into reset when it's unused so we don't + collect spurs from there. However, this means the GPS will need to + warm up when being selected. Selecting the GPS as a source at runtime + is possible, it just won't be available until it's warmed up. + """ + if clock_source not in self.VALID_CLK_EXPORTS: + self.export_clock(False) + if clock_source == self.SOURCE_INTERNAL: + self._set_gps_rstn(0) + self._set_ref_clk_sel_usr(0) + # If we are using an internal PPS, then we terminate the connector + use_pps_term = time_source == self.SOURCE_INTERNAL + self._pps_term.set(use_pps_term) + self._mbrefclk_bias.set(1) + if self._revision >= 2: + self.set_nsync_lmk_power_en(0) + self._lmk05318_bias.set(0) + self._ref_clk_select_net.set(0) + elif clock_source == self.SOURCE_EXTERNAL: + self._set_gps_rstn(0) + self._set_ref_clk_sel_usr(1) + self._exportclk_bias.set(0) + self._pps_term.set(1) + self._mbrefclk_bias.set(1) + if self._revision >= 2: + self.set_nsync_lmk_power_en(0) + self._lmk05318_bias.set(0) + self._ref_clk_select_net.set(0) + elif clock_source == self.SOURCE_GPSDO: + self._assert_gps_supported() + self._set_gps_rstn(1) + self._set_ref_clk_sel_usr(0) + self._pps_term.set(1) + self._mbrefclk_bias.set(1) + if self._revision >= 2: + self.set_nsync_lmk_power_en(0) + self._lmk05318_bias.set(0) + self._ref_clk_select_net.set(0) + elif clock_source == self.SOURCE_NSYNC: + self._check_nsync_supported() + self._set_gps_rstn(0) + self._set_ref_clk_sel_usr(0) + self._tcxo_en.set(1) + self._nsync_refsel.set(1) + self._mbrefclk_bias.set(0) + self._lmk05318_bias.set(1) + self._pps_term.set(1) + self._ref_clk_select_net.set(1) + self._init_nsync_lmk() + else: + raise RuntimeError('Invalid clock source {}'.format(clock_source)) + self._source = clock_source + self.log.trace("set clock source to: {}".format(self._source)) + + + def export_clock(self, enable=True): + """Export clock source to RefOut""" + clock_source = self.get_clock_source() + if not enable: + self._exportclk_bias.set(0) + self._pps_term.set(1) + if self._revision >= 2: + self._exportclk_en.set(0) + elif clock_source in self.VALID_CLK_EXPORTS: + self._exportclk_bias.set(1) + self._pps_term.set(0) + if self._revision >= 2: + self._exportclk_en.set(1) + else: + raise RuntimeError('Invalid source to export: {}'.format(clock_source)) + + def set_trig(self, enable, direction=None): + """Enable/disable the Trig IO out""" + if direction is None: + direction = ClockingAuxBrdControl.DIRECTION_OUTPUT + + if enable: + self._trig_oe_n.set(0) + else: + self._trig_oe_n.set(1) + + if direction == self.DIRECTION_INPUT: + self._trig_dir.set(0) + elif direction == self.DIRECTION_OUTPUT: + self._trig_dir.set(1) + else: + raise RuntimeError( + 'Invalid direction {}, valid options are {} and {}' + .format(direction, self.DIRECTION_INPUT, self.DIRECTION_OUTPUT)) + + def get_clock_source(self): + """Returns the clock source""" + return self._source + + def get_gps_phase_lock(self): + """Returns true if the GPS Phase is locked, and false if it is not""" + return self._gps_phase_lock.get() + + def get_gps_warmup(self): + """Returns true if the GPS is warming up""" + return self._gps_warmup.get() + + def get_gps_survey(self): + """Returns whether or not an auto survey is in progress""" + return self._gps_survey.get() + + def get_gps_lock(self): + """Returns whether or not the GPS has a lock""" + return self._gps_lock.get() + + def get_gps_alarm(self): + """Returns true if the GPS detects a hardware fault or software alarm""" + return self._gps_alarm.get() + + def get_3v3_pg(self): + """Returns true if the 3.3V rail is good, false otherwise""" + return self._3v3_power_good.get() + + def _set_ref_clk_sel_usr(self, value): + """Sets REF_CLK_SEL_USR to value""" + value = int(value) + assert value in (0, 1) + if value == 1: + #Never set REF_CLK_SEL_USR and GPS_RSTn high at the same time, hardware can be damaged + self._set_gps_rstn(0) + self._ref_clk_sel_usr.set(value) + + def _set_gps_rstn(self, value): + """ + Sets GPS_RSTn to value + + If value == 0, then the GPS is held in reset and is not usable. + """ + value = int(value) + assert value in (0, 1) + if value == 1: + # Never set REF_CLK_SEL_USR and GPS_RSTn high at the same time, + # hardware can be damaged + self._set_ref_clk_sel_usr(0) + if self._gps_support: + self._gps_rst_n.set(value) + elif value == 1: + raise RuntimeError("No GPS, so can't bring it out of reset") + + def get_nsync_chip_id_valid(self): + """Returns whether the chip ID of the LMK is valid""" + return self._nsync_pll.is_chip_id_valid() + + def set_nsync_soft_reset(self, value=True): + """Soft reset LMK chip""" + return self._nsync_pll.soft_reset(value) + + def get_nsync_status0(self): + """Returns value of STATUS0 pin on LMK05318 NSYNC IC""" + self._check_nsync_supported() + return self._nsync_status0.get() + + def get_nsync_status1(self): + """Returns value of STATUS1 pin on LMK05318 NSYNC IC""" + self._check_nsync_supported() + return self._nsync_status1.get() + + def set_nsync_pri_ref_source(self, source): + """Sets LMK05318 PRIMREF (primary reference) to specified source""" + self._check_nsync_supported() + + if source not in self.VALID_NSYNC_LMK_PRI_REF_SOURCES: + raise RuntimeError( + "Invalid primary reference clock source for LMK05318 NSYNC IC") + + self.config_dpll(source) + if source == self.SOURCE_NSYNC_LMK_PRI_FABRIC_CLK: + self._fpga_clk_gty_fabric_sel.set(1) + else: + self._fpga_clk_gty_fabric_sel.set(0) + + def set_nsync_ref_select(self, source): + """Sets LMK05318 REFSEL to PRIREF or SECREF""" + self._check_nsync_supported() + if source == self.NSYNC_PRI_REF: + self._nsync_refsel.set(0) + elif source == self.NSYNC_SEC_REF: + self._nsync_refsel.set(1) + else: + raise RuntimeError( + "Invalid setting for LMK05318 NSYNC REFSEL") + + def set_nsync_tcxo_en(self, enable): + """ + Enables/Disables the 10 MHz TCXO chip output; this signal serves as the + oscillator input to the LMK05318 NSYNC IC. + """ + self._check_nsync_supported() + if enable: + self._tcxo_en.set(1) + else: + self._tcxo_en.set(0) + + def set_nsync_lmk_power_en(self, enable): + """Turn on/off the LMK05318 IC using the PDN pin""" + self._check_nsync_supported() + if enable: + self.log.trace("enable LMK05318 power") + self._nsync_power_ctrl.set(1) + else: + self.log.trace("disable LMK05318 power") + self._nsync_power_ctrl.set(0) + + def write_nsync_lmk_cfg_regs_to_eeprom(self, method): + """program the current LMK config to LMK eeprom""" + self._check_nsync_supported() + self.log.trace("LMK05318: store cfg in eeprom") + self._nsync_pll.write_cfg_regs_to_eeprom(method) + + def write_nsync_lmk_eeprom_to_cfg_regs(self): + """read register cfg from eeprom and store it into registers""" + self._check_nsync_supported() + self.log.trace("LMK05318: read cfg from eeprom") + self._nsync_pll.write_eeprom_to_cfg_regs() + + def get_nsync_lmk_eeprom_prog_cycles(self): + """ + returns the number of eeprom programming cycles + note: + the actual counter only increases after programming AND power-cycle/hard-reset + so multiple programming cycles without power cycle will lead to wrong + counter values + """ + self._check_nsync_supported() + return self._nsync_pll.get_eeprom_prog_cycles() + + def get_nsync_lmk_status_dpll(self): + """ + returns the DPLL status register as human readable string + """ + self._check_nsync_supported() + return self._nsync_pll.get_status_dpll() + + def get_nsync_lmk_status_pll_xo(self): + """ + returns the PLL and XO status register as human readable string + """ + self._check_nsync_supported() + return self._nsync_pll.get_status_pll_xo() + + def peek8(self, addr): + """Read from addr over SPI""" + self._check_nsync_supported() + val = self._nsync_pll.peek8(addr) + return val + + def poke8(self, addr, val, overwrite_mask=False): + """ + Write val to addr over SPI + Some register of the LMK IC are supposed not to be written and therefore + the whole register or just some bits. are protected by masking. + If you are really sure what you are doing you can overwrite the masking + by setting overwrite_mask=True + """ + self._check_nsync_supported() + self._nsync_pll.poke8(addr, val, overwrite_mask) + + def config_dpll(self, source): + """ + configures the dpll registers needed to lock to the expected signal + + Initial config files were created with TICSpro, then files were compared + against each other to determine which registers needed to be changed + """ + if source == self.SOURCE_NSYNC_LMK_PRI_GTY_RCV_CLK: + self._nsync_pll.pokes8(( + (0xC5,0x0B), + (0xCC,0x05), + (0xD1,0x08), + (0xD3,0x0A), + (0xD5,0x08), + (0xD7,0x0A), + (0xDA,0x02), + (0xDB,0xFA), + (0xDC,0xF1), + (0xDE,0x06), + (0xDF,0x1A), + (0xE0,0x81), + (0xE3,0x30), + (0xE4,0xD4), + (0xE6,0x06), + (0xE7,0x1A), + (0xE8,0x80), + (0x100,0x00), + (0x101,0x7D), + (0x103,0x08), + (0x109,0x0F), + (0x10A,0xA0), + (0x10F,0x78), + (0x110,0x00), + (0x111,0x00), + (0x112,0x00), + (0x113,0x0F), + (0x114,0x0E), + (0x115,0x0F), + (0x116,0x08), + (0x118,0x08), + (0x119,0x06), + (0x11A,0x08), + (0x11B,0x06), + (0x11E,0x00), + (0x11F,0x71), + (0x121,0xEB), + (0x123,0x09), + (0x128,0x03), + (0x129,0x05), + (0x12A,0x03), + (0x12D,0x3E), + (0x12E,0x3F), + (0x130,0x01), + (0x133,0x01), + (0x134,0x4D), + (0x135,0x55), + (0x136,0x55), + (0x137,0x55), + (0x138,0x55), + (0x139,0x55), + (0x13A,0xFF), + (0x13B,0xFF), + (0x13C,0xFF), + (0x13D,0xFF), + (0x13E,0xFF), + (0x141,0x19), + (0x145,0x78), + (0x147,0x00), + (0x148,0x27), + (0x149,0x10), + (0x14B,0x32), + (0x14F,0x78), + (0x151,0x00), + (0x152,0x27), + (0x153,0x10))) + elif source == self.SOURCE_NSYNC_LMK_PRI_FABRIC_CLK: + self._nsync_pll.pokes8(( + (0xC5,0x0D), + (0xCC,0x07), + (0xD1,0x04), + (0xD3,0x05), + (0xD5,0x04), + (0xD7,0x05), + (0xDA,0x01), + (0xDB,0x2C), + (0xDC,0x00), + (0xDE,0x03), + (0xDF,0x0D), + (0xE0,0x40), + (0xE3,0x18), + (0xE4,0x6A), + (0xE6,0x03), + (0xE7,0x0D), + (0xE8,0x40), + (0x100,0x06), + (0x101,0x00), + (0x103,0x7D), + (0x109,0xF4), + (0x10A,0x24), + (0x10F,0x7A), + (0x110,0x1F), + (0x111,0x1F), + (0x112,0x1F), + (0x113,0x13), + (0x114,0x10), + (0x115,0x13), + (0x116,0x04), + (0x118,0x04), + (0x119,0x02), + (0x11A,0x07), + (0x11B,0x02), + (0x11E,0x02), + (0x11F,0x6C), + (0x121,0xE7), + (0x123,0x25), + (0x128,0x00), + (0x129,0x06), + (0x12A,0x00), + (0x12D,0x17), + (0x12E,0x1B), + (0x130,0x00), + (0x133,0x1E), + (0x134,0x84), + (0x135,0x80), + (0x136,0x00), + (0x137,0x00), + (0x138,0x00), + (0x139,0x00), + (0x13A,0x00), + (0x13B,0x00), + (0x13C,0x00), + (0x13D,0x00), + (0x13E,0x00), + (0x141,0x0A), + (0x145,0x0A), + (0x147,0x03), + (0x148,0x0F), + (0x149,0x49), + (0x14B,0x14), + (0x14F,0x9A), + (0x151,0x03), + (0x152,0x0F), + (0x153,0x49))) + else: + raise RuntimeError( + "Invalid source for dpll programming") + + def set_ref_lock_led(self, val): + """ + Set the reference-locked LED on the back panel + """ + self._ref_lck_led.set(int(val)) diff --git a/mpm/python/usrp_mpm/periph_manager/x4xx_clk_mgr.py b/mpm/python/usrp_mpm/periph_manager/x4xx_clk_mgr.py new file mode 100644 index 000000000..bf845dd65 --- /dev/null +++ b/mpm/python/usrp_mpm/periph_manager/x4xx_clk_mgr.py @@ -0,0 +1,780 @@ +# +# Copyright 2021 Ettus Research, a National Instruments Company +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +""" +X400 Clocking Management + +This module handles the analog clocks on the X4x0 motherboard. The clocking +architecture of the motherboard is spread out between a clocking auxiliary board, +which contains a GPS-displicined OCXO, but also connects an external reference +to the motherboard. It also houses a PLL for deriving a clock from the network +(eCPRI). The clocking aux board has its own control class (ClockingAuxBrdControl) +which also contains controls for the eCPRI PLL. + +The motherboard itself has two main PLLs for clocking purposes: The Sample PLL +(also SPLL) is used to create all clocks used for RF-related purposes. It creates +the sample clock (a very fast clock, ~3 GHz) and the PLL reference clock (PRC) +which is used as a timebase for the daughterboard CPLD and a reference for the +LO synthesizers (50-64 MHz). + +Its input is the base reference clock (BRC). This clock comes either from the +clocking aux board, which in turn can provide a reference from the OCXO (with or +without GPS-disciplining) or from the external reference input SMA port. +The BRC is typically 10-25 MHz. + +The BRC can also come from the reference PLL (RPLL), when the clock source is +set to 'mboard'. The RPLL produces clocks that are consumed by the GTY banks +(for Ethernet and Aurora), but it can also generate a reference clock for +the SPLL. By default, its reference is a fixed 100 MHz clock, but it can also be +driven by the eCPRI PLL, which itself can be driven by a clock from the GTY banks, +which is the case if the clock source is set to 'nsync'. + +The master clock rate (MCR) is not actually controlled in this module, but it +depends on the sample clock rate. It also depends on the RFDC settings, so it is +controlled in x4xx.py, which has access to both RFDC and X4xxClockMgr. + +Block diagram (for more details, see the schematic):: + + ┌────────────────────────────────────────────────────────┐ + │ Clocking Aux Board │ + │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ + │ │ GPSDO ├─> OCXO │ │External│ │eCPRI/ │ │ + │ │ │ │ │ │ │ │nsync │ │ + │ └────────┘ └────┬───┘ └───┬────┘ └────────┘ │ + │ │ │ │ + │ │ │ │ + │ ┌───────────v─────────v───┐ ┌───────────┐ │ + │ │ │ │eCPRI PLL │ │ + │ └┐ MUX ┌┘ │LMK05318 │ │ + │ └─┐ ┌─┘ │ │ │ + │ └─┬─────────────────┘ └──┬────────┘ │ + │ │ │ │ + └───────────┼───────────────────────────┼────────────────┘ + │ │ + │ ┌─────────────┐ │ + ┌──v──v┐ │ │ + │ MUX │ │ │ ┌───── 100 MHz + └──┬───┘ │ │ │ + │Base Ref. Clock │ │ │ + ┌───────v───────┐ │ ┌───────v──v──┐ + │ Sample PLL │ └──┤Reference PLL│ + │ LMK04832 │ │LMK03328 │ + │ │ │ │ + │ │ │ │ + └──┬─────────┬──┘ └────┬────────┘ + │ │ │ + │ │ │ + v v v + Sample PLL Reference GTY Banks + Clock Clock + + +The code in this module controls the RPLL and SPLL as well as some of the muxing. +The eCPRI PLL is controlled from the ClockingAuxBrdControl class. +Most importantly, this class controls the sequencing of configuring clocks. This +means it won't only switch muxes and configure PLLs, but will also do things in +the right order, and put components in reset where necessary. +For this reason, it requires callbacks to reset RFDC and daughterboard clocks. +""" + +from enum import Enum +from usrp_mpm import lib # Pulls in everything from C++-land +from usrp_mpm.sys_utils import i2c_dev +from usrp_mpm.sys_utils.gpio import Gpio +from usrp_mpm.sys_utils.udev import dt_symbol_get_spidev +from usrp_mpm.periph_manager.x4xx_periphs import MboardRegsControl +from usrp_mpm.periph_manager.x4xx_sample_pll import LMK04832X4xx +from usrp_mpm.periph_manager.x4xx_reference_pll import LMK03328X4xx +from usrp_mpm.periph_manager.x4xx_clk_aux import ClockingAuxBrdControl +from usrp_mpm.mpmutils import poll_with_timeout +from usrp_mpm.rpc_server import no_rpc + +# this is not the frequency out of the GPSDO(GPS Lite, 20MHz) itself but +# the GPSDO on the CLKAUX board is used to fine tune the OCXO via EFC +# which is running at 10MHz +X400_GPSDO_OCXO_CLOCK_FREQ = 10e6 +X400_RPLL_I2C_LABEL = 'rpll_i2c' +X400_DEFAULT_RPLL_REF_SOURCE = '100M_reliable_clk' +X400_DEFAULT_MGT_CLOCK_RATE = 156.25e6 +X400_DEFAULT_INT_CLOCK_FREQ = 25e6 + +class X4xxClockMgr: + """ + X4x0 Clocking Manager + + The clocking subsystem of X4x0 is very complex. This class is designed to + capture all clocking-related logic specific to the X4x0. + + This class controls clock and time sources. + """ + CLOCK_SOURCE_MBOARD = "mboard" + CLOCK_SOURCE_INTERNAL = ClockingAuxBrdControl.SOURCE_INTERNAL + CLOCK_SOURCE_EXTERNAL = ClockingAuxBrdControl.SOURCE_EXTERNAL + CLOCK_SOURCE_GPSDO = ClockingAuxBrdControl.SOURCE_GPSDO + CLOCK_SOURCE_NSYNC = ClockingAuxBrdControl.SOURCE_NSYNC + + TIME_SOURCE_INTERNAL = "internal" + TIME_SOURCE_EXTERNAL = "external" + TIME_SOURCE_GPSDO = "gpsdo" + TIME_SOURCE_QSFP0 = "qsfp0" + + # All valid sync_sources for X4xx in the form of (clock_source, time_source) + valid_sync_sources = { + (CLOCK_SOURCE_MBOARD, TIME_SOURCE_INTERNAL), + (CLOCK_SOURCE_INTERNAL, TIME_SOURCE_INTERNAL), + (CLOCK_SOURCE_EXTERNAL, TIME_SOURCE_EXTERNAL), + (CLOCK_SOURCE_EXTERNAL, TIME_SOURCE_INTERNAL), + (CLOCK_SOURCE_GPSDO, TIME_SOURCE_GPSDO), + (CLOCK_SOURCE_GPSDO, TIME_SOURCE_INTERNAL), + (CLOCK_SOURCE_NSYNC, TIME_SOURCE_INTERNAL), + } + + class SetSyncRetVal(Enum): + OK = 'OK' + NOP = 'nop' + FAIL = 'failure' + + def __init__(self, + clock_source, + time_source, + ref_clock_freq, + sample_clock_freq, + is_legacy_mode, + clk_aux_board, + cpld_control, + log): + # Store parent objects + self.log = log.getChild("ClkMgr") + self._cpld_control = cpld_control + self._clocking_auxbrd = clk_aux_board + self._time_source = time_source + self._clock_source = clock_source + self._int_clock_freq = X400_DEFAULT_INT_CLOCK_FREQ + self._ext_clock_freq = ref_clock_freq + # Preallocate other objects to satisfy linter + self.mboard_regs_control = None + self._sample_pll = None + self._reference_pll = None + self._rpll_i2c_bus = None + self._base_ref_clk_select = None + self._set_reset_rfdc = lambda **kwargs: None + self._set_reset_db_clocks = lambda *args: None + self._rpll_reference_sources = {} + # Init peripherals + self._init_available_srcs() + self._init_clk_peripherals() + # Now initialize the clocks themselves + self._init_ref_clock_and_time( + clock_source, + ref_clock_freq, + sample_clock_freq, + is_legacy_mode, + ) + self._init_meas_clock() + self._cpld_control.enable_pll_ref_clk() + + ########################################################################### + # Initialization code + ########################################################################### + def _init_available_srcs(self): + """ + Initialize the available clock and time sources. + """ + has_gps = self._clocking_auxbrd and self._clocking_auxbrd.is_gps_supported() + self._avail_clk_sources = [self.CLOCK_SOURCE_MBOARD] + if self._clocking_auxbrd: + self._avail_clk_sources.extend([ + self.CLOCK_SOURCE_INTERNAL, + self.CLOCK_SOURCE_EXTERNAL]) + if self._clocking_auxbrd.is_nsync_supported(): + self._avail_clk_sources.append(self.CLOCK_SOURCE_NSYNC) + if has_gps: + self._avail_clk_sources.append(self.CLOCK_SOURCE_GPSDO) + self.log.trace(f"Available clock sources are: {self._avail_clk_sources}") + self._avail_time_sources = [ + self.TIME_SOURCE_INTERNAL, self.TIME_SOURCE_EXTERNAL, self.TIME_SOURCE_QSFP0] + if has_gps: + self._avail_time_sources.append(self.TIME_SOURCE_GPSDO) + self.log.trace("Available time sources are: {}".format(self._avail_time_sources)) + + def _init_clk_peripherals(self): + """ + Initialize objects for peripherals owned by this class. Most importantly, + this includes the RPLL and SPLL control classes. + """ + # Create SPI and I2C interfaces to the LMK registers + spll_spi_node = dt_symbol_get_spidev('spll') + sample_lmk_regs_iface = lib.spi.make_spidev_regs_iface( + spll_spi_node, + 1000000, # Speed (Hz) + 0x3, # SPI mode + 8, # Addr shift + 0, # Data shift + 1<<23, # Read flag + 0, # Write flag + ) + # Initialize I2C connection to RPLL + self._rpll_i2c_bus = i2c_dev.dt_symbol_get_i2c_bus(X400_RPLL_I2C_LABEL) + if self._rpll_i2c_bus is None: + raise RuntimeError("RPLL I2C bus could not be found") + reference_lmk_regs_iface = lib.i2c.make_i2cdev_regs_iface( + self._rpll_i2c_bus, + 0x54, # addr + False, # ten_bit_addr + 100, # timeout_ms + 1 # reg_addr_size + ) + self._sample_pll = LMK04832X4xx(sample_lmk_regs_iface, self.log) + self._reference_pll = LMK03328X4xx(reference_lmk_regs_iface, self.log) + # Init BRC select GPIO control + self._base_ref_clk_select = Gpio('BASE-REFERENCE-CLOCK-SELECT', Gpio.OUTPUT, 1) + + def _init_ref_clock_and_time(self, + clock_source, + ref_clock_freq, + sample_clock_freq, + is_legacy_mode, + ): + """ + Initialize clock and time sources. After this function returns, the + reference signals going to the FPGA are valid. + + This is only called once, during __init__(). Calling it again will set + clocks to defaults, but is also redundant since clocks do not need to be + initialized twice. + """ + # A dictionary of tuples (source #, rate) corresponding to each + # available RPLL reference source. + # source # 1 => PRIREF source + # source # 2 => SECREF source + self._rpll_reference_sources = {X400_DEFAULT_RPLL_REF_SOURCE: (2, 100e6)} + reference_rates = [None, None] + for source, rate in self._rpll_reference_sources.values(): + reference_rates[source-1] = rate + self._reference_pll.reference_rates = reference_rates + # Now initializes and reconfigure all clocks. + # If clock_source and ref_clock_freq are not provided, they will not be changed. + # If any other parameters are not provided, they will be configured with + # default values. + self._reset_clocks(value=True, reset_list=['cpld']) + if clock_source is not None: + self._set_brc_source(clock_source) + if ref_clock_freq is not None: + self._set_ref_clock_freq(ref_clock_freq) + self._config_rpll( + X400_DEFAULT_MGT_CLOCK_RATE, + X400_DEFAULT_INT_CLOCK_FREQ, + X400_DEFAULT_RPLL_REF_SOURCE) + self._config_spll(sample_clock_freq, is_legacy_mode) + self._reset_clocks(value=False, reset_list=['cpld']) + + def _init_meas_clock(self): + """ + Initialize the TDC measurement clock. After this function returns, the + FPGA TDC meas_clock is valid. + """ + # This may or may not be used for X400. Leave as a place holder + self.log.debug("TDC measurement clock not yet implemented.") + + ########################################################################### + # Public APIs (that are not exposed as MPM calls) + ########################################################################### + @no_rpc + def set_rfdc_reset_cb(self, rfdc_reset_cb): + """ + Set reference to RFDC control. Ideally, we'd get that in __init__(), but + due to order of operations, it's not ready yet when we call that. + """ + self._set_reset_rfdc = rfdc_reset_cb + + @no_rpc + def set_dboard_reset_cb(self, db_reset_cb): + """ + Set reference to RFDC control. Ideally, we'd get that in __init__(), but + due to order of operations, it's not ready yet when we call that. + """ + self._set_reset_db_clocks = db_reset_cb + + @no_rpc + def unset_cbs(self): + """ + Removes any stored references to our owning X4xx class instance + """ + self._set_reset_rfdc = None + self._set_reset_db_clocks = None + + @no_rpc + def config_pps_to_timekeeper(self, master_clock_rate): + """ Configures the path from the PPS to the timekeeper""" + pps_source = "internal_pps" \ + if self._time_source == self.TIME_SOURCE_INTERNAL \ + else "external_pps" + self._sync_spll_clocks(pps_source) + self._configure_pps_forwarding(True, master_clock_rate) + + @no_rpc + def get_clock_sources(self): + """ + Lists all available clock sources. + """ + return self._avail_clk_sources + + @no_rpc + def get_time_sources(self): + " Returns list of valid time sources " + return self._avail_time_sources + + @no_rpc + def get_ref_clock_freq(self): + " Returns the currently active reference clock frequency (BRC) " + clock_source = self.get_clock_source() + if clock_source == self.CLOCK_SOURCE_MBOARD: + return self._int_clock_freq + if clock_source == self.CLOCK_SOURCE_GPSDO: + return X400_GPSDO_OCXO_CLOCK_FREQ + # clock_source == "external": + return self._ext_clock_freq + + @no_rpc + def get_ref_locked(self): + """ + Return lock status both RPLL and SPLL. + """ + ref_pll_status = self._reference_pll.get_status() + sample_pll_status = self._sample_pll.get_status() + return all([ + ref_pll_status['PLL1 lock'], + ref_pll_status['PLL2 lock'], + sample_pll_status['PLL1 lock'], + sample_pll_status['PLL2 lock'], + ]) + + @no_rpc + def set_spll_rate(self, sample_clock_freq, is_legacy_mode): + """ + Safely set the output rate of the sample PLL. + + This will do the required resets. + """ + self._reset_clocks(value=True, reset_list=('rfdc', 'cpld', 'db_clock')) + self._config_spll(sample_clock_freq, is_legacy_mode) + self._reset_clocks(value=False, reset_list=('rfdc', 'cpld', 'db_clock')) + + @no_rpc + def set_sync_source(self, clock_source, time_source): + """ + Selects reference clock and PPS sources. Unconditionally re-applies the + time source to ensure continuity between the reference clock and time + rates. + Note that if we change the source such that the time source is changed + to 'external', then we need to also disable exporting the reference + clock (RefOut and PPS-In are the same SMA connector). + """ + assert (clock_source, time_source) in self.valid_sync_sources, \ + f'Clock and time source pair ({clock_source}, {time_source}) is ' \ + 'not a valid selection' + # Now see if we can keep the current settings, or if we need to run an + # update of sync sources: + if (clock_source == self._clock_source) and (time_source == self._time_source): + spll_status = self._sample_pll.get_status() + rpll_status = self._reference_pll.get_status() + if (spll_status['PLL1 lock'] and spll_status['PLL2 lock'] and + rpll_status['PLL1 lock'] and rpll_status['PLL2 lock']): + # Nothing change no need to do anything + self.log.trace("New sync source assignment matches " + "previous assignment. Ignoring update command.") + return self.SetSyncRetVal.NOP + self.log.debug( + "Although the clock source has not changed, some PLLs " + "are not locked. Setting clock source again...") + self.log.trace("- SPLL status: {}".format(spll_status)) + self.log.trace("- RPLL status: {}".format(rpll_status)) + # Start setting sync source + self.log.debug( + f"Setting sync source to time_source={time_source}, " + f"clock_source={clock_source}") + self._time_source = time_source + # Reset downstream clocks (excluding RPLL) + self._reset_clocks(value=True, reset_list=('db_clock', 'cpld', 'rfdc', 'spll')) + self._set_brc_source(clock_source) + return self.SetSyncRetVal.OK + + @no_rpc + def set_clock_source_out(self, enable=True): + """ + Allows routing the clock configured as source on the clk aux board to + the RefOut terminal. This only applies to internal, gpsdo and nsync. + """ + clock_source = self.get_clock_source() + if self.get_time_source() == self.TIME_SOURCE_EXTERNAL: + raise RuntimeError( + 'Cannot export clock when using external time reference!') + if clock_source not in self._clocking_auxbrd.VALID_CLK_EXPORTS: + raise RuntimeError(f"Invalid source to export: `{clock_source}'") + if self._clocking_auxbrd is None: + raise RuntimeError("No clocking aux board available") + return self._clocking_auxbrd.export_clock(enable) + + ########################################################################### + # Top-level BIST APIs + # + # These calls will be available as MPM calls. They are only needed by BIST. + ########################################################################### + def enable_ecpri_clocks(self, enable=True, clock='both'): + """ + Enable or disable the export of FABRIC and GTY_RCV eCPRI + clocks. Main use case until we support eCPRI is manufacturing + testing. + """ + self.mboard_regs_control.enable_ecpri_clocks(enable, clock) + + def nsync_change_input_source(self, source): + """ + Switches the input reference source of the clkaux lmk (the "eCPRI PLL"). + + Valid options are: fabric_clk, gty_rcv_clk, and sec_ref. + + fabric_clk and gty_rcv_clk are clock sources from the mboard. + They are both inputs to the primary reference source of the clkaux lmk. + sec_ref is the default reference select for the clkaux lmk, it has + two inputs: Ref in or internal and GPS mode + + Only a public API for the BIST. + """ + assert source in ( + self._clocking_auxbrd.SOURCE_NSYNC_LMK_PRI_FABRIC_CLK, + self._clocking_auxbrd.SOURCE_NSYNC_LMK_PRI_GTY_RCV_CLK, + self._clocking_auxbrd.NSYNC_SEC_REF, + ) + if source == self._clocking_auxbrd.SOURCE_NSYNC_LMK_PRI_FABRIC_CLK: + self.enable_ecpri_clocks(True, 'fabric') + self._clocking_auxbrd.set_nsync_ref_select( + self._clocking_auxbrd.NSYNC_PRI_REF) + self._clocking_auxbrd.set_nsync_pri_ref_source(source) + elif source == self._clocking_auxbrd.SOURCE_NSYNC_LMK_PRI_GTY_RCV_CLK: + self.enable_ecpri_clocks(True, 'gty_rcv') + self._clocking_auxbrd.set_nsync_ref_select( + self._clocking_auxbrd.NSYNC_PRI_REF) + self._clocking_auxbrd.set_nsync_pri_ref_source(source) + else: + self._clocking_auxbrd.set_nsync_ref_select( + self._clocking_auxbrd.NSYNC_SEC_REF) + + def config_rpll_to_nsync(self): + """ + Configures the rpll to use the LMK28PRIRefClk output + by the clkaux LMK + """ + # LMK28PRIRefClk only available when nsync is source, as lmk + # is powered off otherwise + self.set_sync_source(clock_source='nsync', time_source=self._time_source) + + # Add LMK28PRIRefClk as an available RPLL reference source + # 1 => PRIREF source; source is output at 25 MHz + # TODO: enable out4 on LMK + previous_ref_rate = self._reference_pll.reference_rates[0] + self._rpll_reference_sources['clkaux_nsync_clk'] = (1, 25e6) + self._reference_pll.reference_rates[0] = 25e6 + self._config_rpll(X400_DEFAULT_MGT_CLOCK_RATE, + X400_DEFAULT_INT_CLOCK_FREQ, + 'clkaux_nsync_clk') + + # remove clkaux_nsync_clk as a valid reference source for later calls + # to _config_rpll(), it is only valid in this configuration + self._reference_pll.reference_rates[0] = previous_ref_rate + del self._rpll_reference_sources['clkaux_nsync_clk'] + + def get_fpga_aux_ref_freq(self): + """ + Return the tick count of an FPGA counter which measures the width of + the PPS signal on the FPGA_AUX_REF FPGA input using a 40 MHz clock. + Main use case until we support eCPRI is manufacturing testing. + A return value of 0 indicates absence of a valid PPS signal on the + FPGA_AUX_REF line. + """ + return self.mboard_regs_control.get_fpga_aux_ref_freq() + + ########################################################################### + # Top-level APIs + # + # These calls will be available as MPM calls + ########################################################################### + def get_clock_source(self): + " Return the currently selected clock source " + return self._clock_source + + def get_time_source(self): + " Return the currently selected time source " + return self._time_source + + def get_spll_freq(self): + """ Returns the output frequency setting of the SPLL """ + return self._sample_pll.output_freq + + def get_prc_rate(self): + """ + Returns the rate of the PLL Reference Clock (PRC) which is + routed to the daughterboards. + Note: The ref clock will change if the sample clock frequency + is modified. + """ + prc_clock_map = { + 2.94912e9: 61.44e6, + 3e9: 62.5e6, + # 3e9: 50e6, RF Legacy mode will be checked separately + 3.072e9: 64e6, + } + + # RF Legacy Mode always has a PRC rate of 50 MHz + if self._sample_pll.is_legacy_mode: + return 50e6 + # else: + return prc_clock_map[self.get_spll_freq()] + + def set_ref_clk_tuning_word(self, tuning_word, out_select=0): + """ + Set the tuning word for the clocking aux board DAC. This wull update the + tuning word used by the DAC. + """ + if self._clocking_auxbrd is not None: + self._clocking_auxbrd.config_dac(tuning_word, out_select) + else: + raise RuntimeError("No clocking aux board available") + + def get_ref_clk_tuning_word(self, out_select=0): + """ + Get the tuning word configured for the clocking aux board DAC. + """ + if self._clocking_auxbrd is None: + raise RuntimeError("No clocking aux board available") + return self._clocking_auxbrd.read_dac(out_select) + + def store_ref_clk_tuning_word(self, tuning_word): + """ + Store the given tuning word in the clocking aux board ID EEPROM. + """ + if self._clocking_auxbrd is not None: + self._clocking_auxbrd.store_tuning_word(tuning_word) + else: + raise RuntimeError("No clocking aux board available") + + def get_sync_sources(self): + """ + Enumerates permissible sync sources. + """ + return [{ + "time_source": time_source, + "clock_source": clock_source + } for (clock_source, time_source) in self.valid_sync_sources] + + + ########################################################################### + # Low-level controls + ########################################################################### + def _reset_clocks(self, value, reset_list): + """ + Shuts down all clocks downstream to upstream or clears reset on all + clocks upstream to downstream. Specify the list of clocks to reset in + reset_list. The order of clocks specified in the reset_list does not + affect the order in which the clocks are reset. + """ + if value: + self.log.trace("Reset clocks: {}".format(reset_list)) + if 'db_clock' in reset_list: + self._set_reset_db_clocks(value) + if 'cpld' in reset_list: + self._cpld_control.enable_pll_ref_clk(enable=False) + if 'rfdc' in reset_list: + self._set_reset_rfdc(reset=True) + if 'spll' in reset_list: + self._sample_pll.reset(value, hard=True) + if 'rpll' in reset_list: + self._reference_pll.reset(value, hard=True) + else: + self.log.trace("Bring clocks out of reset: {}".format(reset_list)) + if 'rpll' in reset_list: + self._reference_pll.reset(value, hard=True) + if 'spll' in reset_list: + self._sample_pll.reset(value, hard=True) + if 'rfdc' in reset_list: + self._set_reset_rfdc(reset=False) + if 'cpld' in reset_list: + self._cpld_control.enable_pll_ref_clk(enable=True) + if 'db_clock' in reset_list: + self._set_reset_db_clocks(value) + + def _config_rpll(self, usr_clk_rate, internal_brc_rate, internal_brc_source): + """ + Configures the LMK03328 to generate the desired MGT reference clocks + and internal BRC rate. + + Currently, the MGT protocol selection is not supported, but a custom + usr_clk_rate can be generated from PLL1. + + usr_clk_rate - the custom clock rate to generate from PLL1 + internal_brc_rate - the rate to output as the BRC + internal_brc_source - the reference source which drives the RPLL + """ + if internal_brc_source not in self._rpll_reference_sources: + self.log.error('Invalid internal BRC source of {} was selected.' + .format(internal_brc_source)) + raise RuntimeError('Invalid internal BRC source of {} was selected.' + .format(internal_brc_source)) + ref_select = self._rpll_reference_sources[internal_brc_source][0] + + # If the desired rate matches the rate of the primary reference source, + # directly passthrough that reference source + if internal_brc_rate == self._reference_pll.reference_rates[0]: + brc_select = 'bypass' + else: + brc_select = 'PLL' + self._reference_pll.init() + self._reference_pll.config( + ref_select, internal_brc_rate, usr_clk_rate, brc_select) + # The internal BRC rate will only change when _config_rpll is called + # with a new internal BRC rate + self._int_clock_freq = internal_brc_rate + + def _config_spll(self, sample_clock_freq, is_legacy_mode): + """ + Configures the SPLL for the specified master clock rate. + """ + self._sample_pll.init() + self._sample_pll.config(sample_clock_freq, self.get_ref_clock_freq(), + is_legacy_mode) + + def _set_brc_source(self, clock_source): + """ + Switches the Base Reference Clock Source between internal, external, + mboard, and gpsdo using the GPIO pin and clocking aux board control. + internal is a clock source internal to the clocking aux board, but + external to the motherboard. + Should not be called outside of set_sync_source or _init_ref_clock_and_time + without proper reset and reconfig of downstream clocks. + """ + if clock_source == self.CLOCK_SOURCE_MBOARD: + self._base_ref_clk_select.set(1) + if self._clocking_auxbrd: + self._clocking_auxbrd.export_clock(False) + else: + if self._clocking_auxbrd is None: + self.log.error('Invalid BRC selection {}. No clocking aux ' + 'board was found.'.format(clock_source)) + raise RuntimeError('Invalid BRC selection {}'.format(clock_source)) + self._base_ref_clk_select.set(0) + if clock_source == self.CLOCK_SOURCE_EXTERNAL: + # This case is a bit special: We also need to tell the clocking + # aux board if we plan to consume the external time reference or + # not. + time_src_board = \ + ClockingAuxBrdControl.SOURCE_EXTERNAL \ + if self._time_source == self.TIME_SOURCE_EXTERNAL \ + else ClockingAuxBrdControl.SOURCE_INTERNAL + self._clocking_auxbrd.set_source( + ClockingAuxBrdControl.SOURCE_EXTERNAL, time_src_board) + elif clock_source == self.CLOCK_SOURCE_INTERNAL: + self._clocking_auxbrd.set_source(ClockingAuxBrdControl.SOURCE_INTERNAL) + elif clock_source == self.CLOCK_SOURCE_GPSDO: + self._clocking_auxbrd.set_source(ClockingAuxBrdControl.SOURCE_GPSDO) + elif clock_source == self.CLOCK_SOURCE_NSYNC: + self._clocking_auxbrd.set_source(ClockingAuxBrdControl.SOURCE_NSYNC) + else: + self.log.error('Invalid BRC selection {}'.format(clock_source)) + raise RuntimeError('Invalid BRC selection {}'.format(clock_source)) + self._clock_source = clock_source + self.log.debug(f"Base reference clock source is: {clock_source}") + + def _sync_spll_clocks(self, pps_source="internal_pps"): + """ + Synchronize base reference clock (BRC) and PLL reference clock (PRC) + at start of PPS trigger. + + Uses the LMK 04832 pll1_r_divider_sync to synchronize BRC with PRC. + This sync method uses a callback to actual trigger the sync. Before + the trigger is pulled (CLOCK_CTRL_PLL_SYNC_TRIGGER) PPS source is + configured base on current reference clock and pps_source. After sync + trigger the method waits for 1sec for the CLOCK_CTRL_PLL_SYNC_DONE + to go high. + + :param pps_source: select whether internal ("internal_pps") or external + ("external_pps") PPS should be used. This parameter + is taken into account when the current clock source + is external. If the current clock source is set to + internal then this parameter is not taken into + account. + :return: success state of sync call + :raises RuntimeError: for invalid combinations of reference clock and + pps_source + """ + def select_pps(): + """ + Select PPS source based on current clock source and pps_source. + + This returns the bits for the motherboard CLOCK_CTRL register that + control the PPS source. + """ + EXT_PPS = "external_pps" + INT_PPS = "internal_pps" + PPS_SOURCES = (EXT_PPS, INT_PPS) + assert pps_source in PPS_SOURCES, \ + "%s not in %s" % (pps_source, PPS_SOURCES) + + supported_configs = { + (10E6, EXT_PPS): MboardRegsControl.CLOCK_CTRL_PPS_EXT, + (10E6, INT_PPS): MboardRegsControl.CLOCK_CTRL_PPS_INT_10MHz, + (25E6, INT_PPS): MboardRegsControl.CLOCK_CTRL_PPS_INT_25MHz + } + + config = (self.get_ref_clock_freq(), pps_source) + if config not in supported_configs: + raise RuntimeError("Unsupported combination of reference clock " + "(%.2E) and PPS source (%s) for PPS sync." % + config) + return supported_configs[config] + + return self._sample_pll.pll1_r_divider_sync( + lambda: self.mboard_regs_control.pll_sync_trigger(select_pps())) + + def _configure_pps_forwarding(self, enable, master_clock_rate, delay=1.0): + """ + Configures the PPS forwarding to the sample clock domain (master + clock rate). This function assumes _sync_spll_clocks function has + already been executed. + + :param enable: Boolean to choose whether PPS is forwarded to the + sample clock domain. + + :param delay: Delay in seconds from the PPS rising edge to the edge + occurence in the application. This value has to be in + range 0 < x <= 1. In order to forward the PPS signal + from base reference clock to sample clock an aligned + rising edge of the clock is required. This can be + created by the _sync_spll_clocks function. Based on the + greatest common divisor of the two clock rates there + are multiple occurences of an aligned edge each second. + One of these aligned edges has to be chosen for the + PPS forwarding by setting this parameter. + + :return: None, Exception on error + """ + return self.mboard_regs_control.configure_pps_forwarding( + enable, master_clock_rate, self.get_prc_rate(), delay) + + def _set_ref_clock_freq(self, freq): + """ + Tell our USRP what the frequency of the external reference clock is. + + Will throw if it's not a valid value. + """ + if (freq < 1e6) or (freq > 50e6): + raise RuntimeError('External reference clock frequency is out of the valid range.') + if (freq % 40e3) != 0: + # TODO: implement exception of a 50e3 step size for 200MSPS + raise RuntimeError('External reference clock frequency is of incorrect step size.') + self._ext_clock_freq = freq + # If the external source is currently selected we also need to re-apply the + # time_source. This call also updates the dboards' rates. + if self.get_clock_source() == self.CLOCK_SOURCE_EXTERNAL: + self.set_sync_source(self._clock_source, self._time_source) diff --git a/mpm/python/usrp_mpm/periph_manager/x4xx_gps_mgr.py b/mpm/python/usrp_mpm/periph_manager/x4xx_gps_mgr.py new file mode 100644 index 000000000..25a91c19b --- /dev/null +++ b/mpm/python/usrp_mpm/periph_manager/x4xx_gps_mgr.py @@ -0,0 +1,157 @@ +# +# Copyright 2021 Ettus Research, a National Instruments Company +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +""" +X4XX GPS Manager + +Handles GPS-related tasks +""" + +import re +from usrp_mpm.gpsd_iface import GPSDIfaceExtension + +class X4xxGPSMgr: + """ + Manager class for GPS-related actions for the X4XX. + + This also "disables" the sensors when the GPS is not enabled. + """ + def __init__(self, clk_aux_board, log): + assert clk_aux_board and clk_aux_board.is_gps_supported() + self._clocking_auxbrd = clk_aux_board + self.log = log.getChild('GPS') + self.log.trace("Initializing GPSd interface") + self._gpsd = GPSDIfaceExtension() + # To disable sensors, we simply return an empty value if GPS is disabled. + # For TPV, SKY, and GPGGA, we can do this in the same fashion (they are + # very similar). gps_time is different (it returns an int) so for sake + # of simplicity it's defined separately below. + for sensor_name in ('gps_tpv', 'gps_sky', 'gps_gpgga'): + sensor_api = f'get_{sensor_name}_sensor' + setattr( + self, sensor_api, + lambda sensor_name=sensor_name: { + 'name': sensor_name, 'type': 'STRING', + 'unit': '', 'value': 'n/a'} \ + if not self.is_gps_enabled() \ + else getattr(self._gpsd, f'get_{sensor_name}_sensor')() + ) + + def extend(self, context): + """ + Extend 'context' with the sensor methods of this class (get_gps_*_sensor). + If 'context' already has such a method, it is skipped. + + Returns a dictionary compatible to mboard_sensor_callback_map. + """ + new_methods = { + re.search(r"get_(.*)_sensor", method_name).group(1): method_name + for method_name in dir(self) + if not method_name.startswith('_') \ + and callable(getattr(self, method_name)) \ + and method_name.endswith("sensor")} + for method_name in new_methods.values(): + if hasattr(context, method_name): + continue + new_method = getattr(self, method_name) + self.log.trace("%s: Adding %s method", context, method_name) + setattr(context, method_name, new_method) + return new_methods + + def is_gps_enabled(self): + """ + Return True if the GPS is enabled/active. + """ + return self._clocking_auxbrd.is_gps_enabled() + + def get_gps_enabled_sensor(self): + """ + Get enabled status of GPS as a sensor dict + """ + gps_enabled = self.is_gps_enabled() + return { + 'name': 'gps_enabled', + 'type': 'BOOLEAN', + 'unit': 'enabled' if gps_enabled else 'disabled', + 'value': str(gps_enabled).lower(), + } + + def get_gps_locked_sensor(self): + """ + Get lock status of GPS as a sensor dict + """ + gps_locked = self.is_gps_enabled() and \ + bool(self._clocking_auxbrd.get_gps_lock()) + return { + 'name': 'gps_lock', + 'type': 'BOOLEAN', + 'unit': 'locked' if gps_locked else 'unlocked', + 'value': str(gps_locked).lower(), + } + + def get_gps_alarm_sensor(self): + """ + Get alarm status of GPS as a sensor dict + """ + gps_alarm = self.is_gps_enabled() and \ + bool(self._clocking_auxbrd.get_gps_alarm()) + return { + 'name': 'gps_alarm', + 'type': 'BOOLEAN', + 'unit': 'active' if gps_alarm else 'not active', + 'value': str(gps_alarm).lower(), + } + + def get_gps_warmup_sensor(self): + """ + Get warmup status of GPS as a sensor dict + """ + gps_warmup = self.is_gps_enabled() and \ + bool(self._clocking_auxbrd.get_gps_warmup()) + return { + 'name': 'gps_warmup', + 'type': 'BOOLEAN', + 'unit': 'warming up' if gps_warmup else 'warmup done', + 'value': str(gps_warmup).lower(), + } + + def get_gps_survey_sensor(self): + """ + Get survey status of GPS as a sensor dict + """ + gps_survey = self.is_gps_enabled() and \ + bool(self._clocking_auxbrd.get_gps_survey()) + return { + 'name': 'gps_survey', + 'type': 'BOOLEAN', + 'unit': 'survey active' if gps_survey else 'survey not active', + 'value': str(gps_survey).lower(), + } + + def get_gps_phase_lock_sensor(self): + """ + Get phase_lock status of GPS as a sensor dict + """ + gps_phase_lock = self.is_gps_enabled() and \ + bool(self._clocking_auxbrd.get_gps_phase_lock()) + return { + 'name': 'gps_phase_lock', + 'type': 'BOOLEAN', + 'unit': 'phase locked' if gps_phase_lock else 'no phase lock', + 'value': str(gps_phase_lock).lower(), + } + + def get_gps_time_sensor(self): + """ + + """ + if not self.is_gps_enabled(): + return { + 'name': 'gps_time', + 'type': 'INTEGER', + 'unit': 'seconds', + 'value': str(-1), + } + return self._gpsd.get_gps_time_sensor() diff --git a/mpm/python/usrp_mpm/periph_manager/x4xx_mb_cpld.py b/mpm/python/usrp_mpm/periph_manager/x4xx_mb_cpld.py new file mode 100644 index 000000000..927db3360 --- /dev/null +++ b/mpm/python/usrp_mpm/periph_manager/x4xx_mb_cpld.py @@ -0,0 +1,204 @@ +# +# Copyright 2021 Ettus Research, a National Instruments Brand +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +""" +X4xx motherboard CPLD control +""" + +from usrp_mpm import lib # Pulls in everything from C++-land + +def parse_encoded_git_hash(encoded): + git_hash = encoded & 0x0FFFFFFF + tree_dirty = ((encoded & 0xF0000000) > 0) + dirtiness_qualifier = 'dirty' if tree_dirty else 'clean' + return (git_hash, dirtiness_qualifier) + +class MboardCPLD: + """ + Control for the CPLD. + """ + # pylint: disable=bad-whitespace + SIGNATURE_OFFSET = 0x0000 + COMPAT_REV_OFFSET = 0x0004 + OLDEST_COMPAT_REV_OFFSET = 0x0008 + GIT_HASH_OFFSET = 0x0010 + DB_ENABLE_OFFSET = 0x0020 + SERIAL_NO_LO_OFFSET = 0x0034 + SERIAL_NO_HI_OFFSET = 0x0038 + CMI_OFFSET = 0x003C + + # change these revisions only on breaking changes + OLDEST_REQ_COMPAT_REV = 0x20122114 + REQ_COMPAT_REV = 0x20122114 + SIGNATURE = 0x0A522D27 + PLL_REF_CLOCK_ENABLED = 1 << 2 + ENABLE_CLK_DB0 = 1 << 8 + ENABLE_CLK_DB1 = 1 << 9 + ENABLE_PRC = 1 << 10 + DISABLE_CLK_DB0 = 1 << 12 + DISABLE_CLK_DB1 = 1 << 13 + DISABLE_PRC = 1 << 14 + RELEASE_RST_DB0 = 1 << 16 + RELEASE_RST_DB1 = 1 << 17 + ASSERT_RST_DB0 = 1 << 20 + ASSERT_RST_DB1 = 1 << 21 + # pylint: enable=bad-whitespace + + def __init__(self, spi_dev_node, log): + self.log = log.getChild("CPLD") + self.regs = lib.spi.make_spidev_regs_iface( + spi_dev_node, + 1000000, # Speed (Hz) + 0, # SPI mode + 32, # Addr shift + 0, # Data shift + 0, # Read flag + 1<<47 # Write flag + ) + self.poke32 = self.regs.poke32 + self.peek32 = self.regs.peek32 + + def enable_pll_ref_clk(self, enable=True): + """ + Enables or disables the PLL reference clock. + + This makes no assumptions on the prior state of the clock. It does check + if the clock-enable was successful by polling the CPLD, and throws if + not. + """ + def check_pll_enabled(): + return self.peek32(self.DB_ENABLE_OFFSET) & self.PLL_REF_CLOCK_ENABLED + if enable: + self.poke32(self.DB_ENABLE_OFFSET, self.ENABLE_PRC) + if not check_pll_enabled(): + self.log.error("PRC enable failed!") + raise RuntimeError('PRC enable failed!') + return + # Disable PRC: + self.poke32(self.DB_ENABLE_OFFSET, self.DISABLE_PRC) + if check_pll_enabled(): + self.log.error('PRC reset failed!') + raise RuntimeError('PRC reset failed!') + + def enable_daughterboard(self, db_id, enable=True): + """ Enable or disable clock forwarding to a given DB """ + if db_id == 0: + release_reset = self.RELEASE_RST_DB0 + assert_reset = self.ASSERT_RST_DB0 + else: + release_reset = self.RELEASE_RST_DB1 + assert_reset = self.ASSERT_RST_DB1 + value = self.peek32(self.DB_ENABLE_OFFSET) + if enable: + # De-assert reset + value = (value | release_reset) & (~assert_reset) + else: #disable + # Assert reset + value = (value | assert_reset) & (~release_reset) + self.poke32(self.DB_ENABLE_OFFSET, value) + + def enable_daughterboard_support_clock(self, db_id, enable=True): + """ Enable or disable clock forwarding to a given DB """ + if db_id == 0: + clk_enable = self.ENABLE_CLK_DB0 + clk_disable = self.DISABLE_CLK_DB0 + else: + clk_enable = self.ENABLE_CLK_DB1 + clk_disable = self.DISABLE_CLK_DB1 + value = self.peek32(self.DB_ENABLE_OFFSET) + if enable: + # Enable clock + value = (value | clk_enable) & (~clk_disable) + else: #disable + # Disable clock + value = (value | clk_disable) & (~clk_enable) + self.poke32(self.DB_ENABLE_OFFSET, value) + + def check_signature(self): + """ + Assert that the CPLD signature is correct. If the CPLD 'signature' + register returns something unexpectected, throws a RuntimeError. + """ + read_signature = self.peek32(self.SIGNATURE_OFFSET) + if self.SIGNATURE != read_signature: + self.log.error('MB PS CPLD signature {:X} does not match ' + 'expected value {:X}'.format(read_signature, self.SIGNATURE)) + raise RuntimeError('MB PS CPLD signature {:X} does not match ' + 'expected value {:X}'.format(read_signature, self.SIGNATURE)) + + def check_compat_version(self): + """ + Check oldest compatible revision offset of HW against required revision. + The value has to match as the register offsets depends on them. + Furthermore there needs to be a minimum revision to check for existence + of functionality. + """ + cpld_image_compat_revision = self.peek32(self.OLDEST_COMPAT_REV_OFFSET) + if cpld_image_compat_revision < self.OLDEST_REQ_COMPAT_REV: + error_message = ( + 'MB CPLD oldest compatible revision' + f' 0x{cpld_image_compat_revision:08x} is out of date. Update' + f' your CPLD image to 0x{self.OLDEST_REQ_COMPAT_REV:08x}.') + self.log.error(error_message) + raise RuntimeError(error_message) + if cpld_image_compat_revision > self.OLDEST_REQ_COMPAT_REV: + error_message = ( + 'MB CPLD oldest compatible revision' + f' 0x{cpld_image_compat_revision:08x} is unknown. Downgrade' + f' your CPLD image to 0x{self.OLDEST_REQ_COMPAT_REV:08x}.') + self.log.error(error_message) + raise RuntimeError(error_message) + + if not self.has_compat_version(self.REQ_COMPAT_REV): + error_message = ( + "MB CPLD compatible revision is too old. Update your CPLD" + f" image to at least 0x{self.REQ_COMPAT_REV:08x}.") + self.log.error(error_message) + raise RuntimeError(error_message) + + def has_compat_version(self, min_required_version): + """ + Check for a minimum required version. + """ + if min_required_version < self.REQ_COMPAT_REV: + self.log.warning( + "Somebody called MB CPLD has_compat_version with revision" + f" 0x{min_required_version:x} which is older than the mandated" + f" version 0x{self.REQ_COMPAT_REV:x}.") + cpld_image_compat_revision = self.peek32(self.COMPAT_REV_OFFSET) + return cpld_image_compat_revision >= min_required_version + + def trace_git_hash(self): + """ + Trace build of MB CPLD + """ + git_hash_rb = self.peek32(self.GIT_HASH_OFFSET) + (git_hash, dirtiness_qualifier) = parse_encoded_git_hash(git_hash_rb) + self.log.trace("MB CPLD build GIT Hash: {:07x} ({})".format( + git_hash, dirtiness_qualifier)) + + def set_serial_number(self, serial_number): + """ + Set serial number register + """ + assert len(serial_number) > 0 + assert len(serial_number) <= 8 + serial_number_string = str(serial_number, 'ascii') + serial_number_int = int(serial_number_string, 16) + self.poke32(self.SERIAL_NO_LO_OFFSET, serial_number_int & 0xFFFFFFFF) + self.poke32(self.SERIAL_NO_HI_OFFSET, serial_number_int >> 32) + + def set_cmi_device_ready(self, ready=True): + """ + Inform CMI partner that this device is ready for PCI-Express communication. + """ + value = 1 if ready else 0 + self.poke32(self.CMI_OFFSET, value) + + def get_cmi_status(self): + """ + Return true if upstream CMI device was found. + """ + return bool(self.peek32(self.CMI_OFFSET)) diff --git a/mpm/python/usrp_mpm/periph_manager/x4xx_periphs.py b/mpm/python/usrp_mpm/periph_manager/x4xx_periphs.py new file mode 100644 index 000000000..be0487872 --- /dev/null +++ b/mpm/python/usrp_mpm/periph_manager/x4xx_periphs.py @@ -0,0 +1,1445 @@ +# +# Copyright 2019 Ettus Research, a National Instruments Brand +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +""" +X4xx peripherals +""" + +import re +import time +import signal +import struct +from multiprocessing import Process, Event +from statistics import mean +from usrp_mpm import lib # Pulls in everything from C++-land +from usrp_mpm.sys_utils import i2c_dev +from usrp_mpm.sys_utils.gpio import Gpio +from usrp_mpm.sys_utils.uio import UIO +from usrp_mpm.mpmutils import poll_with_timeout +from usrp_mpm.sys_utils.sysfs_thermal import read_thermal_sensor_value +from usrp_mpm.periph_manager.common import MboardRegsCommon + +class DioControl: + """ + DioControl acts as front end for DIO AUX BOARD + + The DioControl class uses three hardware resources to control the behavior + of the board, which are + * I2C extender + * MB registers + * MB cpld registers + + DioControl supports arbitrary methods of addressing the pins on the + frontend. The current implementation supports two ways of pin addressing: + HDMI and DIO. Use set_port_mapping to switch between both of them. + + When using HDMI as pin addressing scheme you have to give the real pin + number of the HDMI adapter like this:: + ┌───────────────────────────────┐ + └┐19 17 15 13 11 09 07 05 03 01┌┘ + └┐ 18 16 14 12 10 08 06 04 02┌┘ + └───────────────────────────┘ + Be aware that not all pins are accessible. The DioControl class will warn + about the usage of unassigned pins. + + The second option is the DIO addressing scheme. Here all user accessible + pins are numbered along the HDMI pin numbers which gives a pin table like + this:: + ┌───────────────────────────────┐ + └┐11 -- 09 08 -- 05 04 -- 01 00┌┘ + └┐ -- 10 -- 07 06 -- 03 02 --┌┘ + └───────────────────────────┘ + + Within the MPM shell one can query the state of the DIO board using + dio_status. This gives an output like this:: + HDMI mapping | PORT A | PORT B + --------------+-------------------------+------------------------- + voltage | OFF - PG:NO - EXT:OFF | 1V8 - PG:YES - EXT:OFF + --------------+-------------------------+------------------------- + master | 0.. 00.0 0.00 .00. 00.1 | 0.. 00.0 0.00 .00. 00.1 + direction | 0.. 00.0 0.00 .00. 00.1 | 0.. 00.0 0.00 .00. 00.0 + --------------+-------------------------+------------------------- + output | 0.. 00.0 0.00 .00. 00.1 | 0.. 00.0 0.00 .00. 00.0 + input | 0.. 00.0 0.00 .00. 00.1 | 0.. 00.0 0.00 .00. 00.0 + The table displays the current state of HDMI port A and B provided by the + DIO board as well as the state of the corresponding register maps and GPIO + pins in a user readable form. + + The header shows the active mapping and the port names. Change the + mapping with set_port_mapping. + + The first row shows the voltage state of each port. Voltage can be one of + the states in (OFF, 1V8, 2V5, 3V3). Change the power state by using + set_voltage_level. When the voltage level is set to OFF, the corresponding + GPIO pin EN_PORT is set low, high otherwise. + When voltage is set to one of the other states EN_PORT<x> is set to high and + EN_PORT<x>_<?V?> is set accordingly where 1V8 corresponds to 2V5 and 3V3 + being both low. PG shows whether the PG (power good) pin corresponding to + the port (PORT<x>_PG) is high. This is NO if power is OFF and YES otherwise. + EXT shows whether EN_EXT_PWR_<x> is enabled for the port. Change the + external power using set_external_power. + Note: A port must have a reasonable voltage level assigned to it before + changes to the output register takes effect on the HDMI port pins or + pin states of the HDMI port can be read in input register. + + The master row shows the pin assignments for the master register which + decides whether PS (1) or FPGA (0) drives output register pins. Change + values using set_master_pin(s). + + The direction row shows the pin assignments for the direction register which + decides whether the pin is written (1) or read (0) by the FPGA. Change + values using set_direction_pin(s). + + The output and input rows shows the pin assignments for the FPGAs input and + output registers. Only the output register pins can be changed. Change + values using set_output_pin(s). + + """ + + # Available DIO ports + DIO_PORTS = ("PORTA", "PORTB") + # Available voltage levels + DIO_VOLTAGE_LEVELS = ("OFF", "1V8", "2V5", "3V3") + + # For each mapping supported by DioControl the class needs the following + # information + # * map_name: name of the mapping (in uppercase) + # * pin_names: names of the pins starting with smallest. Unassignable PINS + # must be named as well + # * port_map: mapping table from FPGA register indices to pin indices. A + # mapping of (4, 2) means pin 4 is mapped to register bit 0 and + # pin2 is mapped to register bit 1. Only assignable pins + # may appear in the mapping. + # * first_pin: index of the first pin for the mapping + + # HDMI mapping constants + HDMI_MAP_NAME = "HDMI" + HDMI_PIN_NAMES = ("Data2+", "Data2_SHD", "Data2-", "Data1+", "Data1_SHD", + "Data1-", "Data0+", "Data0_SHD", "Data0-", "CLK+", + "CLK_SHD", "CLK-", "RESERVED", "HEC_Data-", "SCL", + "SDA", "HEC_GND", "V+", "HEC_Data+") + HDMI_PORT_MAP = {DIO_PORTS[0]: (3, 1, 4, 6, 9, 7, 10, 12, 15, 13, 16, 19), + DIO_PORTS[1]: (16, 19, 15, 13, 10, 12, 9, 7, 4, 6, 3, 1)} + HDMI_FIRST_PIN = 1 + + # DIO mapping constants + DIO_MAP_NAME = "DIO" + DIO_PIN_NAMES = ("DIO0", "DIO1", "DIO2", "DIO3", "DIO4", "DIO5", + "DIO6", "DIO7", "DIO8", "DIO9", "DIO10", "DIO11") + DIO_PORT_MAP = {DIO_PORTS[0]: (1, 0, 2, 3, 5, 4, 6, 7, 9, 8, 10, 11), + DIO_PORTS[1]: (10, 11, 9, 8, 6, 7, 5, 4, 2, 3, 1, 0)} + DIO_FIRST_PIN = 0 + + # Register layout/size constants + PORT_BIT_SIZE = 16 # number of bits used in register per port + PORT_USED_BITS_MASK = 0xFFF # masks out lower 12 of 16 bits used per port + + # DIO registers addresses in FPGA + FPGA_DIO_REGISTER_BASE = 0x2000 + FPGA_DIO_MASTER_REGISTER = FPGA_DIO_REGISTER_BASE + FPGA_DIO_DIRECTION_REGISTER = FPGA_DIO_REGISTER_BASE + 0x4 + FPGA_DIO_INPUT_REGISTER = FPGA_DIO_REGISTER_BASE + 0x8 + FPGA_DIO_OUTPUT_REGISTER = FPGA_DIO_REGISTER_BASE + 0xC + # DIO registers addresses in CPLD + CPLD_DIO_DIRECTION_REGISTER = 0x30 + + class _PortMapDescriptor: + """ + Helper class to hold port mapping relevant information + """ + def __init__(self, name, pin_names, map, first_pin): + self.name = name + self.pin_names = pin_names + self.map = map + self.first_pin = first_pin + + + class _PortControl: + """ + Helper class for controlling ports on the I2C expander + """ + def __init__(self, port): + assert port in DioControl.DIO_PORTS + prefix = "DIOAUX_%s" % port + + self.enable = Gpio('%s_ENABLE' % prefix, Gpio.OUTPUT) + self.en_3v3 = Gpio('%s_3V3' % prefix, Gpio.OUTPUT) + self.en_2v5 = Gpio('%s_2V5' % prefix, Gpio.OUTPUT) + self.ext_pwr = Gpio('%s_ENABLE_EXT_PWR' % prefix, Gpio.OUTPUT) + self.power_good = Gpio('%s_PWR_GOOD' % prefix, Gpio.INPUT) + + + def __init__(self, mboard_regs, mboard_cpld, log): + """ + Initializes access to hardware components as well as creating known + port mappings + :param log: logger to be used for output + """ + self.log = log.getChild(self.__class__.__name__) + self.port_control = {port: self._PortControl(port) for port in self.DIO_PORTS} + self.mboard_regs = mboard_regs + self.mboard_cpld = mboard_cpld + + # initialize port mapping for HDMI and DIO + self.port_mappings = {} + self.mapping = None + self.port_mappings[self.HDMI_MAP_NAME] = self._PortMapDescriptor( + self.HDMI_MAP_NAME, self.HDMI_PIN_NAMES, + self.HDMI_PORT_MAP, self.HDMI_FIRST_PIN) + self.port_mappings[self.DIO_MAP_NAME] = self._PortMapDescriptor( + self.DIO_MAP_NAME, self.DIO_PIN_NAMES, + self.DIO_PORT_MAP, self.DIO_FIRST_PIN) + self.set_port_mapping(self.HDMI_MAP_NAME) + self.log.trace("Spawning DIO fault monitors...") + self._tear_down_monitor = Event() + self._dio0_fault_monitor = Process( + target=self._monitor_dio_fault, + args=('A', "DIO_INT0", self._tear_down_monitor) + ) + self._dio1_fault_monitor = Process( + target=self._monitor_dio_fault, + args=('B', "DIO_INT1", self._tear_down_monitor) + ) + signal.signal(signal.SIGINT, self._monitor_int_handler) + self._dio0_fault_monitor.start() + self._dio1_fault_monitor.start() + + def _monitor_dio_fault(self, dio_port, fault, tear_down): + """ + Monitor the DIO_INT lines to detect an external power fault. + If there is a fault, turn off external power. + """ + self.log.trace("Launching monitor loop...") + fault_line = Gpio(fault, Gpio.FALLING_EDGE) + while True: + try: + if fault_line.event_wait(): + # If we saw a fault, disable the external power + self.log.warning("DIO fault occurred on port {} - turning off external power" + .format(dio_port)) + self.set_external_power(dio_port, 0) + # If the event wait gets interrupted because we are trying to tear down then stop + # the monitoring process. If not, keep monitoring + except InterruptedError: + pass + if tear_down.is_set(): + break + + def _monitor_int_handler(self, signum, frame): + """ + If we see an expected interrupt signal, mark the DIO fault monitors + for tear down. + """ + self._tear_down_monitor.set() + + # -------------------------------------------------------------------------- + # Helper methods + # -------------------------------------------------------------------------- + def _map_to_register_bit(self, port, pin): + """ + Maps a pin denoted in current mapping scheme to a corresponding bit in + the register map. + :param port: port to do the mapping on + :param pin: pin (in current mapping scheme) + :return: bit position in register map + :raises RuntimeError: pin is not in range of current mapping scheme + or not user assignable. + """ + assert isinstance(pin, int) + port = self._normalize_port_name(port) + first_pin = self.mapping.first_pin + last_pin = first_pin + len(self.mapping.pin_names) - 1 + port_map = self.mapping.map[port] + + if not (first_pin <= pin <= last_pin): + raise RuntimeError("Pin must be in range [%d,%d]. Given pin: %d" % + (first_pin, last_pin, pin)) + if pin not in port_map: + raise RuntimeError("Pin %d (%s) is not a user assignable pin." % + (pin, + self.mapping.pin_names[pin - first_pin])) + + # map pin back to register bit + bit = port_map.index(pin) + # lift register bit up by PORT_BIT_SIZE for port b + bit = bit if port == self.DIO_PORTS[0] else bit + self.PORT_BIT_SIZE + return bit + + def _calc_register_value(self, register, port, pin, value): + """ + Recalculates register value. + + Current register state is read and the bit that corresponds to the + values given by port and pin is determined. The register content is + changed at position of bit to what is given by value. + + Note: This routine only reads the current and calculates the new + register value. It is up to the callee to set the register value. + :param register: Address of the register value to recalculate + :param port: port associated with pin + :param pin: pin to change (will be mapped to bit according to + current mapping scheme an given port) + :param value: new bit value to set + :return: new register value. + """ + assert value in [0, 1] + + content = self.mboard_regs.peek32(register) + bit = self._map_to_register_bit(port, pin) + content = (content | 1 << bit) if value == 1 else (content & ~(1 << bit)) + return content + + def _set_pin_values(self, port, values, set_method): + """ + Helper method to assign multiple pins in one call. + :param port: Port to set pins on + :param values: New pin assignment represented by an integer. Each bit of + values corresponds to a pin on board according to current + mapping scheme. Bits that do not correspond to a pin in + the current mapping scheme are skipped. + :param set_method: method to be used to set/unset a pin. Signature of + set_method is (port, pin). + """ + first_pin = self.mapping.first_pin + port = self._normalize_port_name(port) + for i, pin_name in enumerate(self.mapping.pin_names): + if i + first_pin in self.mapping.map[port]: + set_method(port, i + first_pin, int(values & 1 << i != 0)) + + # -------------------------------------------------------------------------- + # Helper to convert abbreviations to constants defined in DioControl + # -------------------------------------------------------------------------- + + def _normalize_mapping(self, mapping): + """ + Map name to one of the key in self.port_mappings. + :param mapping: mapping name or any abbreviation by removing letters + from the end of the name + :return: Key found for mapping name + :raises RuntimeError: no matching mapping could be found + """ + assert isinstance(mapping, str) + mapping = mapping.upper() + mapping_names = self.port_mappings.keys() + try: + # search for abbr of mapping in mapping names + index = [re.match("^%s" % mapping, name) is not None for name in mapping_names].index(True) + return list(self.port_mappings.keys())[index] + except ValueError: + raise RuntimeError("Mapping %s not found in %s" % (mapping, mapping_names)) + + def _normalize_port_name(self, name): + """ + Map port name to the normalized form of self.DIO_PORTS + :param name: port name or abbreviation with A or B, case insensitive + :return: normalized port name + :raises RuntimeError: name could not be normalized + """ + assert isinstance(name, str) + if not name.upper() in self.DIO_PORTS + ("A", "B"): + raise RuntimeError("Could not map %s to port name" % name) + return self.DIO_PORTS[0] if name.upper() in (self.DIO_PORTS[0], "A") \ + else self.DIO_PORTS[1] + + # -------------------------------------------------------------------------- + # Helper to format status output + # -------------------------------------------------------------------------- + + def _get_port_voltage(self, port): + """ + Format voltage table cell value. + """ + port_control = self.port_control[port] + result = "" + if port_control.enable.get() == 0: + result += self.DIO_VOLTAGE_LEVELS[0] + elif port_control.en_2v5.get() == 1: + result += self.DIO_VOLTAGE_LEVELS[2] + elif port_control.en_3v3.get() == 1: + result += self.DIO_VOLTAGE_LEVELS[3] + else: + result += self.DIO_VOLTAGE_LEVELS[1] + result += " - PG:" + result += "YES" if port_control.power_good.get() else "NO" + result += " - EXT:" + result += "ON" if port_control.ext_pwr.get() else "OFF" + return result + + def _get_voltage(self): + """ + Format voltage table cells + """ + return [self._get_port_voltage(port) for port in self.DIO_PORTS] + + def _format_register(self, port, content): + """ + Format a port value according to current mapping scheme. Pins are + grouped by 4. Pins which are not user assignable are marked with a dot. + :param content: register content + :return: register content as pin assignment according to current + mapping scheme + """ + result = "" + first_pin = self.mapping.first_pin + pin_names = self.mapping.pin_names + mapping = self.mapping.map[port] + for i, _ in enumerate(pin_names): + if i % 4 == 0 and i > 0: + result = " " + result + if i + first_pin in mapping: + result = str(int(content & (1 << mapping.index(i + first_pin)) != 0)) + result + else: + result = "." + result + return result + + def _format_registers(self, content): + """ + Formats register content for port A and B + :param content: + :return: + """ + port_a = content & self.PORT_USED_BITS_MASK + port_b = (content >> self.PORT_BIT_SIZE) & self.PORT_USED_BITS_MASK + return [self._format_register(self.DIO_PORTS[0], port_a), + self._format_register(self.DIO_PORTS[1], port_b)] + + def _format_row(self, values, fill=" ", delim="|"): + """ + Format a table row with fix colums widths. Generates row spaces using + value list with empty strings and "-" as fill and "+" as delim. + :param values: cell values (list of three elements) + :param fill: fill character to use (space by default) + :param delim: delimiter character between columns + :return: formated row + """ + col_widths = [14, 25, 25] + return delim.join([ + fill + values[i].ljust(width - len(fill), fill) + for i, width in enumerate(col_widths) + ]) + "\n" + + # -------------------------------------------------------------------------- + # Public API + # -------------------------------------------------------------------------- + + def tear_down(self): + """ + Mark the DIO monitoring processes for tear down and terminate the processes + """ + self._tear_down_monitor.set() + self._dio0_fault_monitor.terminate() + self._dio1_fault_monitor.terminate() + self._dio0_fault_monitor.join(3) + self._dio1_fault_monitor.join(3) + if self._dio0_fault_monitor.is_alive() or \ + self._dio1_fault_monitor.is_alive(): + self.log.warning("DIO monitor didn't exit properly") + + def set_port_mapping(self, mapping): + """ + Change the port mapping to mapping. Mapping must denote a mapping found + in this.port_mappings.keys() or any abbreviation allowed by + _normalize_port_mapping. The mapping does not change the status of the + FPGA registers. It only changes the status display and the way calls + to set_pin_<register_name>(s) are interpreted. + :param mapping: new mapping to be used + :raises RuntimeError: mapping could not be found + """ + assert isinstance(mapping, str) + map_name = self._normalize_mapping(mapping) + if not map_name in self.port_mappings.keys(): + raise RuntimeError("Could not map %s to port mapping" % mapping) + self.mapping = self.port_mappings[map_name] + + def set_pin_master(self, port, pin, value=1): + """ + Set master pin of a port. The master pin decides whether the DIO board + pin is driven by the PS (1) or FPGA (0) register interface. To change + the pin value the current register content is read first and modified + before it is written back, so the register must be readable. + :param port: port to change master assignment on + :param pin: pin to change + :param value: desired pin value + """ + content = self._calc_register_value(self.FPGA_DIO_MASTER_REGISTER, + port, pin, value) + self.mboard_regs.poke32(self.FPGA_DIO_MASTER_REGISTER, content) + + def set_pin_masters(self, port, values): + """ + Set all master pins of a port at once using a bit mask. + :param port: port to change master pin assignment + :param values: New pin assignment represented by an integer. Each bit of + values corresponds to a pin on board according to current + mapping scheme. Bits that do not correspond to a pin in + the current mapping scheme are skipped. + """ + self._set_pin_values(port, values, self.set_pin_master) + + def set_pin_direction(self, port, pin, value=1): + """ + Set direction pin of a port. The direction pin decides whether the DIO + external pin is used as an output (write - value is 1) or input (read - + value is 0). To change the pin value the current register content is + read first and modified before it is written back, so the register must + be readable. + Besides the FPGA register map, the CPLD register map is also written. To + prevent the internal line to be driven by FGPA and DIO board at the same + time the CPLD register is written first if the direction will become an + output. If direction will become an input the FPGA register is written + first. + :param port: port to change direction assignment on + :param pin: pin to change + :param value: desired pin value + """ + content = self._calc_register_value(self.FPGA_DIO_DIRECTION_REGISTER, + port, pin, value) + # When setting direction pin, order matters. Always switch the component + # first that will get the driver disabled. + # This ensures that there wont be two drivers active at a time. + if value == 1: # FPGA is driver => write DIO register first + self.mboard_cpld.poke32(self.CPLD_DIO_DIRECTION_REGISTER, content) + self.mboard_regs.poke32(self.FPGA_DIO_DIRECTION_REGISTER, content) + else: # DIO is driver => write FPGA register first + self.mboard_regs.poke32(self.FPGA_DIO_DIRECTION_REGISTER, content) + self.mboard_cpld.poke32(self.CPLD_DIO_DIRECTION_REGISTER, content) + # Read back values to ensure registers are in sync + cpld_content = self.mboard_cpld.peek32(self.CPLD_DIO_DIRECTION_REGISTER) + mbrd_content = self.mboard_regs.peek32(self.FPGA_DIO_DIRECTION_REGISTER) + if not ((cpld_content == content) and (mbrd_content == content)): + raise RuntimeError("Direction register content mismatch. Expected:" + "0x%0.8X, CPLD: 0x%0.8X, FPGA: 0x%0.8X." % + (content, cpld_content, mbrd_content)) + + def set_pin_directions(self, port, values): + """ + Set all direction pins of a port at once using a bit mask. + :param port: port to change direction pin assignment + :param values: New pin assignment represented by an integer. Each bit of + values corresponds to a pin on board according to current + mapping scheme. Bits that do not correspond to a pin in + the current mapping scheme are skipped. + """ + self._set_pin_values(port, values, self.set_pin_direction) + + def set_pin_output(self, port, pin, value=1): + """ + Set output value of a pin on a port. Setting this value only takes + effect if the direction of the corresponding pin of this port is set + accordingly. To change the pin value the current register content is + read first and modified before it is written back, so the register must + be readable. + :param port: port to change output assignment on + :param pin: pin to change + :param value: desired pin value + """ + content = self._calc_register_value(self.FPGA_DIO_OUTPUT_REGISTER, + port, pin, value) + self.mboard_regs.poke32(self.FPGA_DIO_OUTPUT_REGISTER, content) + + def set_pin_outputs(self, port, values): + """ + Set all output pins of a port at once using a bit mask. + :param port: port to change direction pin assignment + :param values: New pin assignment represented by an integer. Each bit of + values corresponds to a pin on board according to current + mapping scheme. Bits that do not correspond to a pin in + the current mapping scheme are skipped. + """ + self._set_pin_values(port, values, self.set_pin_output) + + def get_pin_input(self, port, pin): + """ + Returns the input pin value of a port. + If the pin is not assignable in the current mapping None is returned. + + :param port: port to read pin value from + :param pin: pin value to read + :returns: actual pin value or None if pin is not assignable + """ + port = self._normalize_port_name(port) + + register = self.mboard_regs.peek32(self.FPGA_DIO_INPUT_REGISTER) + if port == self.DIO_PORTS[1]: + register = register >> self.PORT_BIT_SIZE + register &= self.PORT_USED_BITS_MASK + + mapping = self.mapping.map[port] + if not pin in mapping: + raise RuntimeError("Pin %d (%s) is not a user readable pin." % + (pin, + self.mapping.pin_names[pin - self.mapping.first_pin])) + return 0 if (register & (1 << mapping.index(pin)) == 0) else 1 + + def get_pin_inputs(self, port): + """ + Returns a bit mask of all pins for the given port. + + :param port: port to read input pins from + :returns: Bit map of input pins, each bit of pins corresponds to a pin + on board according to current mapping scheme. Unused pins + stay zero + """ + result = 0 + first_pin = self.mapping.first_pin + pin_names = self.mapping.pin_names + port = self._normalize_port_name(port) + mapping = self.mapping.map[port] + for i, name in enumerate(pin_names): + if i + first_pin in mapping: + if self.get_pin_input(port, i + first_pin): + result |= 1 << i + return result + + def set_voltage_level(self, port, level): + """ + Change voltage level of a port. This is how EN_<port>, EN_<port>_2V5 and + EN_<port>_3V3 are set according to level:: + level EN_<port> EN_<port>_2V5 EN_<port>_3V3 + off 0 0 0 + 1V8 1 0 0 + 2V5 1 1 0 + 3V3 1 0 1 + If level is set to anything other than off this method waits for + <port>_PG to go high. Waiting stops as soon as <port>_PG goes high or + a timeout of 1s occurs. + Note: All pins are set to zero first before the new level is applied. + :param port: port to change power level for + :param level: new power level + :raises RuntimeError: power good pin did not go high + """ + port = self._normalize_port_name(port) + level = level.upper() + assert port in self.DIO_PORTS + assert level in self.DIO_VOLTAGE_LEVELS + port_control = self.port_control[port] + + port_control.enable.set(0) + port_control.en_2v5.set(0) + port_control.en_3v3.set(0) + if level == self.DIO_VOLTAGE_LEVELS[2]: + port_control.en_2v5.set(1) + elif level == self.DIO_VOLTAGE_LEVELS[3]: + port_control.en_3v3.set(1) + + # wait for <port>_PG to go high + if not level == self.DIO_VOLTAGE_LEVELS[0]: # off + port_control.enable.set(1) + if not poll_with_timeout( + lambda: port_control.power_good.get() == 1, 1000, 10): + raise RuntimeError( + "Power good pin did not go high after power up") + + def set_external_power(self, port, value): + """ + Change EN_EXT_PWR_<port> to value. + :param port: port to change external power level for + :param value: 1 to enable external power, 0 to disable + :raise RuntimeError: port or pin value could not be mapped + """ + port = self._normalize_port_name(port) + value = int(value) + assert value in (0, 1) + assert port in self.DIO_PORTS + self.port_control[port].ext_pwr.set(value) + + def status(self): + """ + Build a full status string for the DIO AUX board, including + I2C pin states and register content in a human readable form. + :return: board status + """ + result = "\n" \ + + self._format_row(["%s mapping" % self.mapping.name, self.DIO_PORTS[0], self.DIO_PORTS[1]]) \ + + self._format_row(["", "", ""], "-", "+") \ + + self._format_row(["voltage"] + self._get_voltage()) \ + + self._format_row(["", "", ""], "-", "+") + + register = self.mboard_regs.peek32(self.FPGA_DIO_MASTER_REGISTER) + result += self._format_row(["master"] + self._format_registers(register)) + + register = self.mboard_regs.peek32(self.FPGA_DIO_DIRECTION_REGISTER) + result += self._format_row(["direction"] + self._format_registers(register)) + + result += self._format_row(["", "", ""], "-", "+") + + register = self.mboard_regs.peek32(self.FPGA_DIO_OUTPUT_REGISTER) + result += self._format_row(["output"] + self._format_registers(register)) + + register = self.mboard_regs.peek32(self.FPGA_DIO_INPUT_REGISTER) + result += self._format_row(["input"] + self._format_registers(register)) + return result + + def debug(self): + """ + Create a debug string containing the FPGA register maps. The CPLD + direction register is not part of the string as the DioControl maintains + it in sync with the FPGA direction register. + :return: register states for debug purpose in human readable form. + """ + master = format(self.mboard_regs.peek32(self.FPGA_DIO_MASTER_REGISTER), "032b") + direction = format(self.mboard_regs.peek32(self.FPGA_DIO_DIRECTION_REGISTER), "032b") + output = format(self.mboard_regs.peek32(self.FPGA_DIO_OUTPUT_REGISTER), "032b") + input = format(self.mboard_regs.peek32(self.FPGA_DIO_INPUT_REGISTER), "032b") + return "\nmaster: " + " ".join(re.findall('....', master)) + "\n" + \ + "direction: " + " ".join(re.findall('....', direction)) + "\n" + \ + "output: " + " ".join(re.findall('....', output)) + "\n" + \ + "input: " + " ".join(re.findall('....', input)) + + +class MboardRegsControl(MboardRegsCommon): + """ + Control the FPGA Motherboard registers + + A note on using this object: All HDL-specifics should be encoded in this + class. That means that calling peek32 or poke32 on this class should only be + used in rare exceptions. The call site shouldn't have to know about + implementation details behind those registers. + + To do multiple peeks/pokes without opening and closing UIO objects, it is + possible to do that from outside the object: + >>> with mb_regs_control.regs: + ... mb_regs_control.poke32(addr0, data0) + ... mb_regs_control.poke32(addr1, data1) + """ + # Motherboard registers + # pylint: disable=bad-whitespace + # 0 through 0x14 are common regs (see MboardRegsCommon) + MB_CLOCK_CTRL = 0x0018 + MB_PPS_CTRL = 0x001C + MB_BUS_CLK_RATE = 0x0020 + MB_BUS_COUNTER = 0x0024 + MB_GPIO_CTRL = 0x002C + MB_GPIO_MASTER = 0x0030 + MB_GPIO_RADIO_SRC = 0x0034 + # MB_NUM_TIMEKEEPERS = 0x0048 Shared with MboardRegsCommon + MB_SERIAL_NO_LO = 0x004C + MB_SERIAL_NO_HI = 0x0050 + MB_MFG_TEST_CTRL = 0x0054 + MB_MFG_TEST_STATUS = 0x0058 + # QSFP port info consists of 2 ports of 4 lanes each, + # both separated by their corresponding stride value + MB_QSFP_PORT_INFO = 0x0060 + MB_QSFP_LANE_STRIDE = 0x4 + MB_QSFP_PORT_STRIDE = 0x10 + # Versioning registers + MB_VER_FPGA = 0x0C00 + MB_VER_CPLD_IFC = 0x0C10 + MB_VER_RF_CORE_DB0 = 0x0C20 + MB_VER_RF_CORE_DB1 = 0x0C30 + MB_VER_GPIO_IFC_DB0 = 0x0C40 + MB_VER_GPIO_IFC_DB1 = 0x0C50 + CURRENT_VERSION_OFFSET = 0x0 + OLDEST_COMPATIBLE_VERSION_OFFSET = 0x4 + VERSION_LAST_MODIFIED_OFFSET = 0x8 + # Timekeeper registers start at 0x1000 (see MboardRegsCommon) + + # Clock control register bit masks + CLOCK_CTRL_PLL_SYNC_DELAY = 0x00FF0000 + CLOCK_CTRL_PLL_SYNC_DONE = 0x00000200 + CLOCK_CTRL_PLL_SYNC_TRIGGER = 0x00000100 + CLOCK_CTRL_TRIGGER_IO_SEL = 0x00000030 + CLOCK_CTRL_TRIGGER_PPS_SEL = 0x00000003 + + MFG_TEST_CTRL_GTY_RCV_CLK_EN = 0x00000001 + MFG_TEST_CTRL_FABRIC_CLK_EN = 0x00000002 + MFG_TEST_AUX_REF_FREQ = 0x03FFFFFF + # Clock control register values + CLOCK_CTRL_TRIG_IO_INPUT = 0 + CLOCK_CTRL_PPS_INT_25MHz = 0 + CLOCK_CTRL_TRIG_IO_PPS_OUTPUT = 0x10 + CLOCK_CTRL_PPS_INT_10MHz = 0x1 + CLOCK_CTRL_PPS_EXT = 0x2 + # pylint: enable=bad-whitespace + + def __init__(self, label, log): + MboardRegsCommon.__init__(self, label, log) + def peek32(address): + """ + Safe peek (opens and closes UIO). + """ + with self.regs: + return self.regs.peek32(address) + def poke32(address, value): + """ + Safe poke (opens and closes UIO). + """ + with self.regs: + self.regs.poke32(address, value) + # MboardRegsCommon.poke32() and ...peek32() don't open the UIO, so we + # overwrite them with "safe" versions that do open the UIO. + self.peek32 = peek32 + self.poke32 = poke32 + + def set_serial_number(self, serial_number): + """ + Set serial number register + """ + assert len(serial_number) > 0 + assert len(serial_number) <= 8 + serial_number = serial_number + b'\x00' * (8 - len(serial_number)) + (sn_lo, sn_hi) = struct.unpack("II", serial_number) + with self.regs: + self.poke32(self.MB_SERIAL_NO_LO, sn_lo) + self.poke32(self.MB_SERIAL_NO_HI, sn_hi) + + def get_compat_number(self): + """get FPGA compat number + + This function reads back FPGA compat number. + The return is a tuple of + 2 numbers: (major compat number, minor compat number) + X4xx stores this tuple in MB_VER_FPGA instead of MB_COMPAT_NUM + """ + version = self.peek32(self.MB_VER_FPGA + self.CURRENT_VERSION_OFFSET) + major = (version >> 23) & 0x1FF + minor = (version >> 12) & 0x7FF + return (major, minor) + + def get_db_gpio_ifc_version(self, slot_id): + """ + Get the version of the DB GPIO interface for the corresponding slot + """ + if slot_id == 0: + current_version = self.peek32(self.MB_VER_GPIO_IFC_DB0) + elif slot_id == 1: + current_version = self.peek32(self.MB_VER_GPIO_IFC_DB1) + else: + raise RuntimeError("Invalid daughterboard slot id: {}".format(slot_id)) + + major = (current_version>>23) & 0x1ff + minor = (current_version>>12) & 0x7ff + build = current_version & 0xfff + return (major, minor, build) + + def _get_qsfp_lane_value(self, port, lane): + addr = self.MB_QSFP_PORT_INFO + (port * self.MB_QSFP_PORT_STRIDE) \ + + (lane * self.MB_QSFP_LANE_STRIDE) + return (self.peek32(addr) >> 8) & 0xFF + + def _get_qsfp_type(self, port=0): + """ + Read the type of qsfp port is in the specified port + """ + x4xx_qsfp_types = { + 0: "", # Port not connected + 1: "1G", + 2: "10G", + 3: "A", # Aurora + 4: "W", # White Rabbit + 5: "100G" + } + + lane_0_val = self._get_qsfp_lane_value(port, 0) + num_lanes = 1 + + # Because we have qsfp, we could have up to 4x connection at the port + if lane_0_val > 0: + for lane in range(1, 4): + lane_val = self._get_qsfp_lane_value(port, lane) + if lane_val == lane_0_val: + num_lanes += 1 + + if num_lanes > 1: + return str(num_lanes) + "x" + x4xx_qsfp_types.get(lane_0_val, "") + + return x4xx_qsfp_types.get(lane_0_val, "") + + def get_fpga_type(self): + """ + Reads the type of the FPGA image currently loaded + Returns a string with the type (ie CG, XG, C2, etc.) + """ + x4xx_fpga_types_by_qsfp = { + ("", ""): "", + ("10G", "10G"): "XG", + ("10G", ""): "X1", + ("2x10G", ""): "X2", + ("4x10G", ""): "X4", + ("4x10G", "100G"): "X4C", + ("100G", "100G"): "CG", + ("100G", ""): "C1" + } + + qsfp0_type = self._get_qsfp_type(0) + qsfp1_type = self._get_qsfp_type(1) + self.log.trace("QSFP types: ({}, {})".format(qsfp0_type, qsfp1_type)) + try: + fpga_type = x4xx_fpga_types_by_qsfp[(qsfp0_type, qsfp1_type)] + except KeyError: + self.log.warning("Unrecognized QSFP type combination: ({}, {})" + .format(qsfp0_type, qsfp1_type)) + fpga_type = "" + + if not fpga_type and self.is_pcie_present(): + fpga_type = "LV" + + return fpga_type + + def is_pcie_present(self): + """ + Return True in case the PCI_EXPRESS_BIT is set in the FPGA image, which + means there is a PCI-Express core. False otherwise. + """ + regs_val = self.peek32(self.MB_DEVICE_ID) + return (regs_val & 0x80000000) != 0 + + def is_pll_sync_done(self): + """ + Return state of PLL sync bit from motherboard clock control register. + """ + return bool(self.peek32(MboardRegsControl.MB_CLOCK_CTRL) & \ + self.CLOCK_CTRL_PLL_SYNC_DONE) + + def pll_sync_trigger(self, clock_ctrl_pps_src): + """ + Callback for LMK04832 driver to actually trigger the sync. Set PPS + source accordingly. + """ + with self.regs: + # Update clock control config register to use the currently relevant + # PPS source + config = self.peek32(self.MB_CLOCK_CTRL) + trigger_config = \ + (config & ~MboardRegsControl.CLOCK_CTRL_TRIGGER_PPS_SEL) \ + | clock_ctrl_pps_src + # trigger sync with appropriate configuration + self.poke32( + self.MB_CLOCK_CTRL, + trigger_config | self.CLOCK_CTRL_PLL_SYNC_TRIGGER) + # wait for sync done indication from FPGA + # The following value is in ms, it was experimentally picked. + pll_sync_timeout = 1500 # ms + result = poll_with_timeout(self.is_pll_sync_done, pll_sync_timeout, 10) + # de-assert sync trigger signal + self.poke32(self.MB_CLOCK_CTRL, trigger_config) + if not result: + self.log.error("PLL_SYNC_DONE not received within timeout") + return result + + def set_trig_io_output(self, enable_output): + """ + Enable TRIG I/O output. If enable_output is "True", then we set TRIG I/O + to be an output. If enable_output is "False", then we make it an input. + + Note that the clocking board also is involved in configuring the TRIG I/O. + This is only the part that configures the FPGA registers. + """ + with self.regs: + # prepare clock control FPGA register content + clock_ctrl_reg = self.peek32(MboardRegsControl.MB_CLOCK_CTRL) + clock_ctrl_reg &= ~self.CLOCK_CTRL_TRIGGER_IO_SEL + if enable_output: + clock_ctrl_reg |= self.CLOCK_CTRL_TRIG_IO_PPS_OUTPUT + else: + # for both input and off ensure FPGA does not drive trigger IO line + clock_ctrl_reg |= self.CLOCK_CTRL_TRIG_IO_INPUT + self.poke32(MboardRegsControl.MB_CLOCK_CTRL, clock_ctrl_reg) + + def configure_pps_forwarding(self, enable, master_clock_rate, prc_rate, delay): + """ + Configures the PPS forwarding to the sample clock domain (master + clock rate). This function assumes _sync_spll_clocks function has + already been executed. + + :param enable: Boolean to choose whether PPS is forwarded to the + sample clock domain. + + :param master_clock_rate: Master clock rate in MHz + + :param prc_rate: PRC rate in MHz + + :param delay: Delay in seconds from the PPS rising edge to the edge + occurence in the application. This value has to be in + range 0 < x <= 1. In order to forward the PPS signal + from base reference clock to sample clock an aligned + rising edge of the clock is required. This can be + created by the _sync_spll_clocks function. Based on the + greatest common divisor of the two clock rates there + are multiple occurences of an aligned edge each second. + One of these aligned edges has to be chosen for the + PPS forwarding by setting this parameter. + + :return: None, Exception on error + """ + # delay range check 0 < x <= 1 + if (delay <= 0 or delay > 1): + raise RuntimeError("The delay has to be in range 0 < x <= 1") + + with self.regs: + # configure delay in BRC clock domain + value = self.peek32(self.MB_CLOCK_CTRL) + pll_sync_delay = (value >> 16) & 0xFF + # pps_brc_delay constants required by HDL implementation + pps_brc_delay = pll_sync_delay + 2 - 1 + value = (value & 0x00FFFFFF) | (pps_brc_delay << 24) + self.poke32(self.MB_CLOCK_CTRL, value) + + # configure delay in PRC clock domain + # reduction by 4 required by HDL implementation + pps_prc_delay = (int(delay * prc_rate) - 4) & 0x3FFFFFF + if pps_prc_delay == 0: + # limitation from HDL implementation + raise RuntimeError("The calculated delay has to be greater than 0") + value = pps_prc_delay + + # configure clock divider + # reduction by 2 required by HDL implementation + prc_rc_divider = (int(master_clock_rate/prc_rate) - 2) & 0x3 + value = value | (prc_rc_divider << 28) + + # write configuration to PPS control register (with PPS disabled) + self.poke32(self.MB_PPS_CTRL, value) + + # enables PPS depending on parameter + if enable: + # wait for 1 second to let configuration settle for any old PPS pulse + time.sleep(1) + # update value with enabled PPS + value = value | (1 << 31) + # write final configuration to PPS control register + self.poke32(self.MB_PPS_CTRL, value) + return True + + def enable_ecpri_clocks(self, enable, clock): + """ + Enable or disable the export of FABRIC and GTY_RCV eCPRI + clocks. Main use case until we support eCPRI is manufacturing + testing. + """ + valid_clocks_list = ['gty_rcv', 'fabric', 'both'] + assert clock in valid_clocks_list + clock_enable_sig = 0 + if clock == 'gty_rcv': + clock_enable_sig = self.MFG_TEST_CTRL_GTY_RCV_CLK_EN + elif clock == 'fabric': + clock_enable_sig = self.MFG_TEST_CTRL_FABRIC_CLK_EN + else:# 'both' case + clock_enable_sig = (self.MFG_TEST_CTRL_GTY_RCV_CLK_EN | + self.MFG_TEST_CTRL_FABRIC_CLK_EN) + with self.regs: + clock_ctrl_reg = self.peek32(MboardRegsControl.MB_MFG_TEST_CTRL) + if enable: + clock_ctrl_reg |= clock_enable_sig + else: + clock_ctrl_reg &= ~clock_enable_sig + self.poke32(MboardRegsControl.MB_MFG_TEST_CTRL, clock_ctrl_reg) + + def get_fpga_aux_ref_freq(self): + """ + Return the tick count of an FPGA counter which measures the width of + the PPS signal on the FPGA_AUX_REF FPGA input using a 40 MHz clock. + Main use case until we support eCPRI is manufacturing testing. + A return value of 0 indicates absence of a valid PPS signal on the + FPGA_AUX_REF line. + """ + status_reg = self.peek32(self.MB_MFG_TEST_STATUS) + return status_reg & self.MFG_TEST_AUX_REF_FREQ + + +class CtrlportRegs: + """ + Control the FPGA Ctrlport registers + """ + # pylint: disable=bad-whitespace + IPASS_OFFSET = 0x000010 + MB_PL_SPI_CONFIG = 0x000020 + DB_SPI_CONFIG = 0x000024 + MB_PL_CPLD = 0x008000 + DB_0_CPLD = 0x010000 + DB_1_CPLD = 0x018000 + # pylint: enable=bad-whitespace + + min_mb_cpld_spi_divider = 2 + min_db_cpld_spi_divider = 5 + class MbPlCpldIface: + """ Exposes access to register mapped MB PL CPLD register space """ + SIGNATURE_OFFSET = 0x0000 + REVISION_OFFSET = 0x0004 + + SIGNATURE = 0x3FDC5C47 + MIN_REQ_REVISION = 0x20082009 + + def __init__(self, regs_iface, offset, log): + self.log = log + self.offset = offset + self.regs = regs_iface + + def peek32(self, addr): + return self.regs.peek32(addr + self.offset) + + def poke32(self, addr, val): + self.regs.poke32(addr + self.offset, val) + + def check_signature(self): + read_signature = self.peek32(self.SIGNATURE_OFFSET) + if self.SIGNATURE != read_signature: + self.log.error('MB PL CPLD signature {:X} does not match ' + 'expected value {:X}'.format(read_signature, self.SIGNATURE)) + raise RuntimeError('MB PL CPLD signature {:X} does not match ' + 'expected value {:X}'.format(read_signature, self.SIGNATURE)) + + def check_revision(self): + read_revision = self.peek32(self.REVISION_OFFSET) + if read_revision < self.MIN_REQ_REVISION: + error_message = ('MB PL CPLD revision {:X} is out of date. ' + 'Expected value {:X}. Update your CPLD image.' + .format(read_revision, self.MIN_REQ_REVISION)) + self.log.error(error_message) + raise RuntimeError(error_message) + + class DbCpldIface: + """ Exposes access to register mapped DB CPLD register spaces """ + def __init__(self, regs_iface, offset): + self.offset = offset + self.regs = regs_iface + + def peek32(self, addr): + return self.regs.peek32(addr + self.offset) + + def poke32(self, addr, val): + self.regs.poke32(addr + self.offset, val) + + def __init__(self, label, log): + self.log = log.getChild("CtrlportRegs") + self._regs_uio_opened = False + try: + self.regs = UIO( + label=label, + read_only=False + ) + except RuntimeError: + self.log.warning('Ctrlport regs could not be found. ' \ + 'MPM Endpoint to the FPGA is not part of this image.') + self.regs = None + # Initialize SPI interface to MB PL CPLD and DB CPLDs + self.set_mb_pl_cpld_divider(self.min_mb_cpld_spi_divider) + self.set_db_divider_value(self.min_db_cpld_spi_divider) + self.mb_pl_cpld_regs = self.MbPlCpldIface(self, self.MB_PL_CPLD, self.log) + self.mb_pl_cpld_regs.check_signature() + self.mb_pl_cpld_regs.check_revision() + self.db_0_regs = self.DbCpldIface(self, self.DB_0_CPLD) + self.db_1_regs = self.DbCpldIface(self, self.DB_1_CPLD) + + def init(self): + if not self._regs_uio_opened: + self.regs._open() + self._regs_uio_opened = True + + def deinit(self): + if self._regs_uio_opened: + self.regs._close() + self._regs_uio_opened = False + + def peek32(self, addr): + if self.regs is None: + raise RuntimeError('The ctrlport registers were never configured!') + if self._regs_uio_opened: + return self.regs.peek32(addr) + else: + with self.regs: + return self.regs.peek32(addr) + + def poke32(self, addr, val): + if self.regs is None: + raise RuntimeError('The ctrlport registers were never configured!') + if self._regs_uio_opened: + return self.regs.poke32(addr, val) + else: + with self.regs: + return self.regs.poke32(addr, val) + + def set_mb_pl_cpld_divider(self, divider_value): + if not self.min_mb_cpld_spi_divider <= divider_value <= 0xFFFF: + self.log.error('Cannot set MB CPLD SPI divider to invalid value {}' + .format(divider_value)) + raise RuntimeError('Cannot set MB CPLD SPI divider to invalid value {}' + .format(divider_value)) + self.poke32(self.MB_PL_SPI_CONFIG, divider_value) + + def set_db_divider_value(self, divider_value): + if not self.min_db_cpld_spi_divider <= divider_value <= 0xFFFF: + self.log.error('Cannot set DB SPI divider to invalid value {}' + .format(divider_value)) + raise RuntimeError('Cannot set DB SPI divider to invalid value {}' + .format(divider_value)) + self.poke32(self.DB_SPI_CONFIG, divider_value) + + def get_db_cpld_iface(self, db_id): + return self.db_0_regs if db_id == 0 else self.db_1_regs + + def get_mb_pl_cpld_iface(self): + return self.mb_pl_cpld_regs + + def enable_cable_present_forwarding(self, enable=True): + value = 1 if enable else 0 + self.poke32(self.IPASS_OFFSET, value) + + +# QSFP Adapter IDs according to SFF-8436 rev 4.9 table 30 +QSFP_IDENTIFIERS = { + 0x00: "Unknown or unspecified", + 0x01: "GBIC", + 0x02: "Module/connector soldered to motherboard (using SFF-8472)", + 0x03: "SFP/SFP+/SFP28", + 0x04: "300 pin XBI", + 0x05: "XENPAK", + 0x06: "XFP", + 0x07: "XFF", + 0x08: "XFP-E", + 0x09: "XPAK", + 0x0A: "X2", + 0x0B: "DWDM-SFP/SFP+ (not using SFF-8472)", + 0x0C: "QSFP (INF-8438)", + 0x0D: "QSFP+ or later (SFF-8436, SFF-8635, SFF-8665, SFF-8685 et al)", + 0x0E: "CXP or later", + 0x0F: "Shielded Mini Multilane HD0x4X", + 0x10: "Shielded Mini Multilane HD0x8X", + 0x11: "QSFP28 or later (SFF-8665 et al)", + 0x12: "CXP2 (aka CXP28) or later", + 0x13: "CDFP (Style0x1/Style2)", + 0x14: "Shielded Mini Multilane HD0x4X Fanout Cable", + 0x15: "Shielded Mini Multilane HD0x8X Fanout Cable", + 0x16: "CDFP (Style0x3)" +} + +# QSFP revison compliance according to SFF-8636 rev 2.9 table 6-3 +QSFP_REVISION_COMPLIANCE = { + 0x00: "Not specified.", + 0x01: "SFF-8436 Rev 4.8 or earlier", + 0x02: "SFF-8436 Rev 4.8 or earlier (except 0x186-0x189)", + 0x03: "SFF-8636 Rev 1.3 or earlier", + 0x04: "SFF-8636 Rev 1.4", + 0x05: "SFF-8636 Rev 1.5", + 0x06: "SFF-8636 Rev 2.0", + 0x07: "SFF-8636 Rev 2.5, 2.6 and 2.7", + 0x08: "SFF-8636 Rev 2.8 or later" +} + +# QSFP connector types according to SFF-8029 rev 3.2 table 4-3 +QSFP_CONNECTOR_TYPE = { + 0x00: "Unknown or unspecified", + 0x01: "SC (Subscriber Connector)", + 0x02: "Fibre Channel Style 1 copper connector", + 0x03: "Fibre Channel Style 2 copper connector", + 0x04: "BNC/TNC (Bayonet/Threaded Neill-Concelman)", + 0x05: "Fibre Channel coax headers", + 0x06: "Fiber Jack", + 0x07: "LC (Lucent Connector)", + 0x08: "MT-RJ (Mechanical Transfer - Registered Jack)", + 0x09: "MU (Multiple Optical)", + 0x0A: "SG", + 0x0B: "Optical Pigtail", + 0x0C: "MPO 1x12 (Multifiber Parallel Optic)", + 0x0D: "MPO 2x16", + 0x20: "HSSDC II (High S peed Serial Data Connector)", + 0x21: "Copper pigtail", + 0x22: "RJ45 (Registered Jack)", + 0x23: "No separable connector", + 0x24: "MXC 2x16" +} + +class QSFPModule: + """ + QSFPModule enables access to the I2C register interface of an QSFP module. + + The class queries the module register using I2C commands according to + SFF-8486 rev 4.9 specification. + """ + + def __init__(self, gpio_modprs, gpio_modsel, devsymbol, log): + """ + modprs: Name of the GPIO pin that reports module presence + modsel: Name of the GPIO pin that controls ModSel of QSFP module + devsymbol: Symbol name of the device used for I2C communication + """ + + self.log = log.getChild('QSFP') + + # Hold the ModSelL GPIO low for communication over I2C. Because X4xx + # uses a I2C switch to communicate with the QSFP modules we can keep + # ModSelL low all the way long, because each QSFP module has + # its own I2C address (see SFF-8486 rev 4.9, chapter 4.1.1.1). + self.modsel = Gpio(gpio_modsel, Gpio.OUTPUT, 0) + + # ModPrs pin read pin MODPRESL from QSFP connector + self.modprs = Gpio(gpio_modprs, Gpio.INPUT, 0) + + # resolve device node name for I2C communication + devname = i2c_dev.dt_symbol_get_i2c_bus(devsymbol) + + # create an object to access I2C register interface + self.qsfp_regs = lib.i2c.make_i2cdev_regs_iface( + devname, # dev node name + 0x50, # start address according to SFF-8486 rev 4.9 chapter 7.6 + False, # use 7 bit address schema + 100, # timeout_ms + 1 # reg_addr_size + ) + + def _peek8(self, address): + """ + Helper method to read bytes from the I2C register interface. + + This helper returns None in case of failed communication + (e.g. missing or broken adapter). + """ + try: + return self.qsfp_regs.peek8(address) + except RuntimeError as err: + self.log.debug("Could not read QSFP register ({})".format(err)) + return None + + def _revision_compliance(self, status): + """ + Map the revison compliance status byte to a human readable string + according to SFF-8636 rev 2.9 table 6-3 + """ + assert isinstance(status, int) + assert 0 <= status <= 255 + if status > 0x08: + return "Reserved" + return QSFP_REVISION_COMPLIANCE[status] + + def is_available(self): + """ + Checks whether QSFP adapter is available by checking modprs pin + """ + return self.modprs.get() == 0 #modprs is active low + + def enable_i2c(self, enable): + """ + Enable or Disable I2C communication with QSFP module. Use with + care. Because X4xx uses an I2C switch to address the QSFP ports + there is no need to drive the modsel high (inactive). Disabled + I2C communication leads to unwanted result when query module + state even if the module reports availability. + """ + self.modsel.set("0" if enable else "1") #modsel is active low + + def adapter_id(self): + """ + Returns QSFP adapter ID as a byte (None if not present) + """ + return self._peek8(0) + + def adapter_id_name(self): + """ + Maps QSFP adapter ID to a human readable string according + to SFF-8436 rev 4.9 table 30 + """ + adapter_id = self.adapter_id() + if adapter_id is None: + return adapter_id + assert isinstance(adapter_id, int) + assert 0 <= adapter_id <= 255 + if adapter_id > 0x7F: + return "Vendor Specific" + if adapter_id > 0x16: + return "Reserved" + return QSFP_IDENTIFIERS[adapter_id] + + def status(self): + """ + Return the 2 byte QSFP adapter status according to SFF-8636 + rev 2.9 table 6-2 + """ + compliance = self._peek8(1) + status = self._peek8(2) + if compliance is None or status is None: + return None + assert isinstance(compliance, int) + assert isinstance(status, int) + return (compliance, status) + + def decoded_status(self): + """ + Decode the 2 status bytes of the QSFP adapter into a tuple + of human readable strings. See SFF-8436 rev 4.9 table 17 + """ + status = self.status() + if not status: + return None + return ( + self._revision_compliance(status[0]), + "Flat mem" if status[1] & 0b100 else "Paged mem", + "IntL asserted" if status[1] & 0b010 else "IntL not asserted", + "Data not ready" if status[1] & 0b001 else "Data ready" + ) + + def vendor_name(self): + """ + Return vendor name according to SFF-8436 rev 4.9 chapter 7.6.2.14 + """ + content = [self._peek8(i) for i in range(148, 163)] + + if all(content): # list must not contain any None values + # convert ASCII codes to string and strip whitespaces at the end + return "".join([chr(i) for i in content]).rstrip() + + return None + + def connector_type(self): + """ + Return connector type according to SFF-8029 rev 3.2 table 4-3 + """ + ctype = self._peek8(130) + if ctype is None: + return None + assert isinstance(ctype, int) + assert 0 <= ctype <= 255 + + if (0x0D < ctype < 0x20) or (0x24 < ctype < 0x80): + return "Reserved" + if ctype > 0x7F: + return "Vendor Specific" + return QSFP_CONNECTOR_TYPE[ctype] + + def info(self): + """ + Human readable string of important QSFP module information + """ + if self.is_available(): + status = self.decoded_status() + return "Vendor name: {}\n" \ + "id: {}\n" \ + "Connector type: {}\n" \ + "Compliance: {}\n" \ + "Status: {}".format( + self.vendor_name(), self.adapter_id_name(), + self.connector_type(), status[0], status[1:]) + + return "No module detected" + +def get_temp_sensor(sensor_names, reduce_fn=mean, log=None): + """ Get temperature sensor reading from X4xx. """ + temps = [] + try: + for sensor_name in sensor_names: + temp_raw = read_thermal_sensor_value( + sensor_name, 'in_temp_raw', 'iio', 'name') + temp_offset = read_thermal_sensor_value( + sensor_name, 'in_temp_offset', 'iio', 'name') + temp_scale = read_thermal_sensor_value( + sensor_name, 'in_temp_scale', 'iio', 'name') + # sysfs-bus-iio linux kernel API reports temp in milli deg C + # https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-bus-iio + temp_in_deg_c = (temp_raw + temp_offset) * temp_scale / 1000 + temps.append(temp_in_deg_c) + except ValueError: + if log: + log.warning("Error when converting temperature value.") + temps = [-1] + except KeyError: + if log: + log.warning("Can't read %s temp sensor fron iio sub-system.", + str(sensor_name)) + temps = [-1] + return { + 'name': 'temperature', + 'type': 'REALNUM', + 'unit': 'C', + 'value': str(reduce_fn(temps)) + } diff --git a/mpm/python/usrp_mpm/periph_manager/x4xx_reference_pll.py b/mpm/python/usrp_mpm/periph_manager/x4xx_reference_pll.py new file mode 100644 index 000000000..4e3a26de2 --- /dev/null +++ b/mpm/python/usrp_mpm/periph_manager/x4xx_reference_pll.py @@ -0,0 +1,339 @@ +# +# Copyright 2019 Ettus Research, a National Instruments Brand +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +""" +LMK03328 driver for use with X4xx +""" + +from time import sleep +from usrp_mpm.chips import LMK03328 +from usrp_mpm.sys_utils.gpio import Gpio + +class LMK03328X4xx(LMK03328): + """ + X4xx-specific subclass of the Reference Clock PLL LMK03328 controls. + """ + def __init__(self, pll_regs_iface, log=None): + LMK03328.__init__(self, pll_regs_iface, log) + + self._pll_status_0 = Gpio('REFERENCE-CLOCK-PLL-STATUS-0', Gpio.INPUT) + self._pll_status_1 = Gpio('REFERENCE-CLOCK-PLL-STATUS-1', Gpio.INPUT) + self._pll_pwrdown_n = Gpio('REFERENCE-CLOCK-PLL-PWRDOWN', Gpio.OUTPUT, 1) + + self._reference_rates = None + + @property + def reference_rates(self): + """ + Gets a list of reference source rates indexed by [primary, secondary] + """ + return self._reference_rates + + @reference_rates.setter + def reference_rates(self, reference_rates): + """ + Sets a list of reference source rates indexed by [primary, secondary] + """ + assert len(reference_rates) == 2, 'Invalid number of reference rates' + self._reference_rates = reference_rates + + def init(self): + """ + Perform a soft reset and verify chip ID + """ + # Clear hard reset + self.reset(False, hard=True) + + # Enable sync mute + self.poke8(0x0C, 0xC8) + + # Trigger soft reset + self.reset(True, hard=False) + self.reset(False, hard=False) + + if not self.verify_chip_id(): + raise Exception("unable to locate LMK03328!") + + def reset(self, value=True, hard=False): + """ + Perform a hard reset from the GPIO pins or a soft reset from the LMK register + """ + if hard: + # The powerdown pin is active low + self._pll_pwrdown_n.set(not value) + else: + self.soft_reset(value) + + def get_status(self): + """ + Returns PLL lock and outgoing status indicators for the LMK03328 + """ + status_indicator_0 = self._pll_status_0.get() + status_indicator_1 = self._pll_status_1.get() + status_indicator = (status_indicator_1 << 1) | status_indicator_0 + return {'PLL1 lock': self.check_pll_locked(1), + 'PLL2 lock': self.check_pll_locked(2), + 'status indicator': status_indicator} + + def config(self, ref_select=2, brc_rate=25e6, usr_clk_rate=156.25e6, brc_select='PLL'): + """ + Configure the RPLL to generate the desired MGT Reference clock sources + using the specified internal BRC. + ref_select - the reference source to use (primary=1, secondary=2) + brc_rate - specifies the desired rate of the output BRC + usr_clk_rate - specifies the desired rate to configure PLL1 + brc_select - specifies whether the BRC out should be from the PLL ('PLL') or + a passthrough of the primary reference signal ('bypass') + """ + def calculate_out7_mux(brc_select): + """ + Returns the OUT7 Mux select register value based on the chosen BRC source. + Note that OUT7 is wired to the InternalRef clock which is used as the default + reference clock source. + """ + return {'bypass': 0x98, 'PLL': 0x58}[brc_select] + def calculate_out7_div(brc_rate): + """ Returns the OUT7 Divider register value based on the chosen BRC rate """ + return {25e6: 0x31, 125e6: 0x09}[brc_rate] + def calculate_pll2_input_select(ref_select): + """ Returns the reference mux register value based on which reference should be used """ + assert ref_select in (1, 2) + return {1: 0x5B, 2: 0x7F}[ref_select] + def calculate_pll2_n_div(ref_rate): + """ Returns the PLL2 N div value based on the rate of the reference source """ + return {25e6: 0x00C8, 100e6: 0x0032}[ref_rate] + def calculate_pll1_post_div(usr_clk_rate): + """ Returns the PLL1 post div value based the usr_clk_rate """ + assert usr_clk_rate in (156.25e6, 125e6, 312.5e6, 161.1328125e6) + return { + 156.25e6: 0x0E, + 125e6: 0x0E, + 312.5e6: 0x0E, + 161.1328125e6: 0x1E, + }[usr_clk_rate] + def calculate_pll1_n_div(usr_clk_rate): + """ Returns the PLL1 N div value based the usr_clk_rate """ + assert usr_clk_rate in (156.25e6, 125e6, 312.5e6, 161.1328125e6) + return { + 156.25e6: 0x0339, + 125e6: 0x0339, + 312.5e6: 0x0032, + 161.1328125e6: 0x0339, + }[usr_clk_rate] + def calculate_pll1_m_div(usr_clk_rate): + """ Returns the PLL1 M div value based the usr_clk_rate """ + assert usr_clk_rate in (156.25e6, 125e6, 312.5e6, 161.1328125e6) + return { + 156.25e6: 0x0F, + 125e6: 0x0F, + 312.5e6: 0x00, + 161.1328125e6: 0x0F, + }[usr_clk_rate] + def calculate_pll_select(usr_clk_rate): + """ Returns the PLL selection based on the usr_clk_rate """ + assert usr_clk_rate in (156.25e6, 125e6) + return {156.25e6: 2, 125e6: 2}[usr_clk_rate] + def get_register_from_pll(pll_selection, addr): + """ Returns the value to write to a specified register given + the desired PLL selection. """ + assert pll_selection in (1, 2) + assert addr in (0x22, 0x29) + return {0x22: [0x00, 0x80], 0x29: [0x10, 0x50]}[addr][pll_selection-1] + def calculate_out_div(usr_clk_rate): + """ Returns the output divider for a given clock rate """ + assert usr_clk_rate in (156.25e6, 125e6) + return {156.25e6: 0x07, 125e6: 0x09}[usr_clk_rate] + + if self._reference_rates is None: + self.log.error('Cannot config reference PLL until the reference sources are set.') + raise RuntimeError('Cannot config reference PLL until the reference sources are set.') + if ref_select not in (1, 2): + raise RuntimeError('Selected reference source {} is invalid'.format(ref_select)) + ref_rate = self._reference_rates[ref_select-1] + if ref_rate not in (25e6, 100e6): + raise RuntimeError('Selected reference rate {} Hz is invalid'.format(ref_rate)) + if brc_select not in ('bypass', 'PLL'): + raise RuntimeError('Selected BRC source {} is invalid'.format(brc_select)) + if brc_rate not in (25e6, 125e6): + raise RuntimeError('Selected BRC rate {} Hz is invalid'.format(brc_rate)) + if brc_select == 'bypass': + # 'bypass' sends the primary reference directly to out7 + actual_brc_rate = self._reference_rates[0] + if actual_brc_rate != brc_rate: + self.log.error('The specified BRC rate does not match the actual ' + 'rate of the primary ref in bypass mode.') + raise RuntimeError('The specified BRC rate does not match the actual ' + 'rate of the primary ref in bypass mode.') + if usr_clk_rate not in (156.25e6, 125e6): + raise RuntimeError('Selected RPLL clock rate {} Hz is not supported'.format(usr_clk_rate)) + + self.log.trace("Configuring RPLL to ref:{}, brc:{} {} Hz, clock rate:{}" + .format(ref_select, brc_select, brc_rate, usr_clk_rate)) + # Config + pll2_input_mux = calculate_pll2_input_select(ref_select) + pll2_n_div = calculate_pll2_n_div(ref_rate) + pll1_post_div = calculate_pll1_post_div(usr_clk_rate) + pll1_n_div = calculate_pll1_n_div(usr_clk_rate) + pll1_m_div = calculate_pll1_m_div(usr_clk_rate) + pll_select = calculate_pll_select(usr_clk_rate) + out_div = calculate_out_div(usr_clk_rate) + out7_mux = calculate_out7_mux(brc_select) + out7_div = calculate_out7_div(brc_rate) + + self.pokes8(( + (0x0C, 0xDF), + (0x0D, 0x00), + (0x0E, 0x00), + (0x0F, 0x00), + (0x10, 0x00), + (0x11, 0x00), + (0x12, 0x00), + (0x13, 0x00), + (0x14, 0xFF), + (0x15, 0xFF), + (0x16, 0xFF), + (0x17, 0x00), # Status 0/1 mute control is disabled. Both status always ON. + (0x18, 0x00), + (0x19, 0x55), + (0x1A, 0x00), + (0x1B, 0x58), + (0x1C, 0x58), + (0x1D, 0x8F), + (0x1E, 0x01), + (0x1F, 0x00), + (0x20, 0x00), + (0x21, 0x00), + (0x22, get_register_from_pll(pll_select, 0x22)), + (0x23, 0x20), + (0x24, out_div), + (0x25, 0xD0), + (0x26, 0x00), + (0x27, 0xD0), + (0x28, 0x09), + (0x29, get_register_from_pll(pll_select, 0x29)), + (0x2A, out_div), + (0x2B, out7_mux), + (0x2C, out7_div), + (0x2D, 0x0A), # Disable all PLL divider status outputs. Both status pins are set to normal operation. + (0x2E, 0x00), # Disable all PLL divider status outputs. + (0x2F, 0x00), # Disable all PLL divider status outputs. + (0x30, 0xFF), # Hidden register. Value from TICS software. + (0x31, 0x0A), # set both status slew rate to slow (2.1 ns) + (0x32, pll2_input_mux), + (0x33, 0x03), + (0x34, 0x00), + (0x35, pll1_m_div), + (0x36, 0x00), + (0x37, 0x00), + (0x38, pll1_post_div), # PLL1 enabled, PLL1 output reset sync enable + (0x39, 0x08), + (0x3A, (pll1_n_div & 0x0F00) >> 8), # PLL1 N Divider [11:8] + (0x3B, (pll1_n_div & 0x00FF) >> 0), # PLL1 N Divider [7:0] + (0x3C, 0x00), + (0x3D, 0x00), + (0x3E, 0x00), + (0x3F, 0x00), + (0x40, 0x00), + (0x41, 0x01), + (0x42, 0x0C), + (0x43, 0x08), # PLL1 loop filter R2 = 735 ohms + (0x44, 0x00), # PLL1 loop filter C1 = 5 pF + (0x45, 0x00), # PLL1 loop filter R3 = 18 ohms + (0x46, 0x00), # PLL1 loop filter C3 = 0 pF + (0x47, 0x0E), + (0x48, 0x08), + (0x49, (pll2_n_div & 0x0F00) >> 8), # PLL2 N Divider [11:8] + (0x4A, (pll2_n_div & 0x00FF) >> 0), # PLL2 N Divider [7:0] + (0x4B, 0x00), + (0x4C, 0x00), + (0x4D, 0x00), + (0x4E, 0x00), + (0x4F, 0x00), + (0x50, 0x01), + (0x51, 0x0C), + (0x52, 0x08), + (0x53, 0x00), + (0x54, 0x00), + (0x55, 0x00), + (0x56, 0x08), + (0x57, 0x00), + (0x58, 0x00), + (0x59, 0xDE), + (0x5A, 0x01), + (0x5B, 0x18), + (0x5C, 0x01), + (0x5D, 0x4B), + (0x5E, 0x01), + (0x5F, 0x86), + (0x60, 0x01), + (0x61, 0xBE), + (0x62, 0x01), + (0x63, 0xFE), + (0x64, 0x02), + (0x65, 0x47), + (0x66, 0x02), + (0x67, 0x9E), + (0x68, 0x00), + (0x69, 0x00), + (0x6A, 0x05), + (0x6B, 0x0F), + (0x6C, 0x0F), + (0x6D, 0x0F), + (0x6E, 0x0F), + (0x6F, 0x00), + (0x70, 0x00), + (0x71, 0x00), + (0x72, 0x00), + (0x73, 0x08), + (0x74, 0x19), + (0x75, 0x00), + (0x76, 0x03), # PLL1 uses 2nd order loop filter recommended for integer PLL mode. + (0x77, 0x01), + (0x78, 0x00), + (0x79, 0x0F), + (0x7A, 0x0F), + (0x7B, 0x0F), + (0x7C, 0x0F), + (0x7D, 0x00), + (0x7E, 0x00), + (0x7F, 0x00), + (0x80, 0x00), + (0x81, 0x08), + (0x82, 0x19), + (0x83, 0x00), + (0x84, 0x03), # PLL2 uses 2nd order loop filter recommended for integer PLL mode. + (0x85, 0x01), + (0x86, 0x00), + (0x87, 0x00), + (0x88, 0x00), + (0x89, 0x10), + (0x8A, 0x00), + (0x8B, 0x00), + (0x8C, 0x00), + (0x8D, 0x00), + (0x8E, 0x00), + (0x8F, 0x00), + (0x90, 0x00), + (0x91, 0x00), + (0xA9, 0x40), + (0xAC, 0x24), + (0xAD, 0x00), + (0x0C, 0x5F), # Initiate VCO calibration + (0x0C, 0xDF), + )) + # wait for VCO calibration to be done and PLL to lock + sleep(0.5) + # Reset all output and PLL post dividers + self.pokes8(( + (0x0C, 0x9F), + (0x0C, 0xDF) + )) + + # Check for Lock + if not self.check_pll_locked(1): + raise RuntimeError('PLL1 did not lock!') + if not self.check_pll_locked(2): + raise RuntimeError('PLL2 did not lock!') + self.log.trace("PLLs are locked!") diff --git a/mpm/python/usrp_mpm/periph_manager/x4xx_rfdc_ctrl.py b/mpm/python/usrp_mpm/periph_manager/x4xx_rfdc_ctrl.py new file mode 100644 index 000000000..44c4a32e2 --- /dev/null +++ b/mpm/python/usrp_mpm/periph_manager/x4xx_rfdc_ctrl.py @@ -0,0 +1,480 @@ +# +# Copyright 2019 Ettus Research, a National Instruments Company +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +""" +X400 RFDC Control Module +""" + +import ast +from collections import OrderedDict +from usrp_mpm import lib # Pulls in everything from C++-land +from usrp_mpm.periph_manager.x4xx_rfdc_regs import RfdcRegsControl +from usrp_mpm.rpc_server import no_rpc + +# Map the interpolation/decimation factor to fabric words. +# Keys: is_dac (False -> ADC, True -> DAC) and factor +FABRIC_WORDS_ARRAY = { # [is_dac][factor] + False: {0: 16, 1: 16, 2: 8, 4: 4, 8: 2}, # ADC + True: {0: -1, 1: -1, 2: 16, 4: 8, 8: 4} # DAC +} + +RFDC_DEVICE_ID = 0 + +class X4xxRfdcCtrl: + """ + Control class for the X4xx's RFDC + + """ + # Label for RFDC UIO + rfdc_regs_label = "rfdc-regs" + # Describes the mapping of ADC/DAC Tiles and Blocks to DB Slot IDs + # Follows the below structure: + # <slot_idx> + # 'adc': [ (<tile_number>, <block_number>), ... ] + # 'dac': [ (<tile_number>, <block_number>), ... ] + RFDC_DB_MAP = [ + { + 'adc': [(0, 1), (0, 0)], + 'dac': [(0, 0), (0, 1)], + }, + { + 'adc': [(2, 1), (2, 0)], + 'dac': [(1, 0), (1, 1)], + }, + ] + + # Maps all possible master_clock_rate (data clk rate * data SPC) values to the + # corresponding sample rate, expected FPGA decimation, whether to configure + # the SPLL in legacy mode (which uses a different divider), and whether half-band + # resampling is used. + # Using an OrderedDict to use the first rates as a preference for the default + # rate for its corresponding decimation. + master_to_sample_clk = OrderedDict({ + # MCR: (SPLL, decimation, legacy mode, half-band resampling) + 122.88e6*4: (2.94912e9, 2, False, False), # RF (1M-8G) + 122.88e6*2: (2.94912e9, 2, False, True), # RF (1M-8G) + 122.88e6*1: (2.94912e9, 8, False, False), # RF (1M-8G) + 125e6*4: (3.00000e9, 2, False, False), # RF (1M-8G) + 200e6: (3.00000e9, 4, True, False), # RF (Legacy Mode) + }) + + + def __init__(self, get_spll_freq, log): + self.log = log.getChild('RFDC') + self._get_spll_freq = get_spll_freq + self._rfdc_regs = RfdcRegsControl(self.rfdc_regs_label, self.log) + self._rfdc_ctrl = lib.rfdc.rfdc_ctrl() + self._rfdc_ctrl.init(RFDC_DEVICE_ID) + + self.set_cal_frozen(1, 0, "both") + self.set_cal_frozen(1, 1, "both") + + @no_rpc + def unset_cbs(self): + """ + Removes any stored references to our owning X4xx class instance + """ + self._get_spll_freq = None + + ########################################################################### + # Public APIs (not available as MPM RPC calls) + ########################################################################### + @no_rpc + def set_reset(self, reset=True): + """ + Resets the RFDC FPGA components or takes them out of reset. + """ + if reset: + # Assert RFDC AXI-S, filters and associated gearbox reset. + self._rfdc_regs.set_reset_adc_dac_chains(reset=True) + self._rfdc_regs.log_status() + # Assert Radio clock PLL reset + self._rfdc_regs.set_reset_mmcm(reset=True) + # Resetting the MMCM will automatically disable clock buffers + return + + # Take upstream MMCM out of reset + self._rfdc_regs.set_reset_mmcm(reset=False) + + # Once the MMCM has locked, enable driving the clocks + # to the rest of the design. Poll lock status for up + # to 1 ms + self._rfdc_regs.wait_for_mmcm_locked(timeout=0.001) + self._rfdc_regs.set_gated_clock_enables(value=True) + + # De-assert RF signal chain reset + self._rfdc_regs.set_reset_adc_dac_chains(reset=False) + + # Restart tiles in XRFdc + # All ADC Tiles + if not self._rfdc_ctrl.reset_tile(-1, False): + self.log.warning('Error starting up ADC tiles') + # All DAC Tiles + if not self._rfdc_ctrl.reset_tile(-1, True): + self.log.warning('Error starting up DAC tiles') + + # Set sample rate for all active tiles + active_converters = set() + for db_idx, db_info in enumerate(self.RFDC_DB_MAP): + db_rfdc_resamp, _ = self._rfdc_regs.get_rfdc_resampling_factor(db_idx) + for converter_type, tile_block_set in db_info.items(): + for tile, block in tile_block_set: + is_dac = converter_type != 'adc' + active_converter_tuple = (tile, block, db_rfdc_resamp, is_dac) + active_converters.add(active_converter_tuple) + for tile, block, resampling_factor, is_dac in active_converters: + self._rfdc_ctrl.reset_mixer_settings(tile, block, is_dac) + self._rfdc_ctrl.set_sample_rate(tile, is_dac, self._get_spll_freq()) + self._set_interpolation_decimation(tile, block, is_dac, resampling_factor) + + self._rfdc_regs.log_status() + + # Set RFDC NCO reset event source to analog SYSREF + for tile, block, _, is_dac in active_converters: + self._rfdc_ctrl.set_nco_event_src(tile, block, is_dac) + + + @no_rpc + def sync(self): + """ + Multi-tile Synchronization on both ADC and DAC + """ + # These numbers are determined from the procedure mentioned in + # PG269 section "Advanced Multi-Tile Synchronization API use". + adc_latency = 1228 # ADC delay in sample clocks + dac_latency = 800 # DAC delay in sample clocks + + # Ideally, this would be a set to avoiding duplicate indices, + # but we need to use a list for compatibility with the rfdc_ctrl + # C++ interface (std::vector) + adc_tiles_to_sync = [] + dac_tiles_to_sync = [] + + rfdc_map = self.RFDC_DB_MAP + for db_id in rfdc_map: + for converter_type, tile_block_set in db_id.items(): + for tile, _ in tile_block_set: + if converter_type == 'adc': + if tile not in adc_tiles_to_sync: + adc_tiles_to_sync.append(tile) + else: # dac + if tile not in dac_tiles_to_sync: + dac_tiles_to_sync.append(tile) + + self._rfdc_ctrl.sync_tiles(adc_tiles_to_sync, False, adc_latency) + self._rfdc_ctrl.sync_tiles(dac_tiles_to_sync, True, dac_latency) + + # We expect all sync'd tiles to have equal latencies + # Sets don't add duplicates, so we can use that to look + # for erroneous tiles + adc_tile_latency_set = set() + for tile in adc_tiles_to_sync: + adc_tile_latency_set.add( + self._rfdc_ctrl.get_tile_latency(tile, False)) + if len(adc_tile_latency_set) != 1: + raise RuntimeError("ADC tiles failed to sync properly") + + dac_tile_latency_set = set() + for tile in dac_tiles_to_sync: + dac_tile_latency_set.add( + self._rfdc_ctrl.get_tile_latency(tile, True)) + if len(dac_tile_latency_set) != 1: + raise RuntimeError("DAC tiles failed to sync properly") + + @no_rpc + def get_default_mcr(self): + """ + Gets the default master clock rate based on FPGA decimation + """ + fpga_decimation, fpga_halfband = self._rfdc_regs.get_rfdc_resampling_factor(0) + for master_clock_rate in self.master_to_sample_clk: + _, decimation, _, halfband = self.master_to_sample_clk[master_clock_rate] + if decimation == fpga_decimation and fpga_halfband == halfband: + return master_clock_rate + raise RuntimeError('No master clock rate acceptable for current fpga ' + 'with decimation of {}'.format(fpga_decimation)) + + @no_rpc + def get_dsp_bw(self): + """ + Return the bandwidth encoded in the RFdc registers. + + Note: This is X4xx-specific, not RFdc-specific. But this class owns the + access to RfdcRegsControl, and the bandwidth is strongly related to the + RFdc settings. + """ + return self._rfdc_regs.get_fabric_dsp_info(0)[0] + + @no_rpc + def get_rfdc_resampling_factor(self, db_idx): + """ + Returns a tuple resampling_factor, halfbands. + + See RfdcRegsControl.get_rfdc_resampling_factor(). + """ + return self._rfdc_regs.get_rfdc_resampling_factor(db_idx) + + + ########################################################################### + # Public APIs that get exposed as MPM RPC calls + ########################################################################### + def rfdc_set_nco_freq(self, direction, slot_id, channel, freq): + """ + Sets the RFDC NCO Frequency for the specified channel + """ + converters = self._find_converters(slot_id, direction, channel) + assert len(converters) == 1 + (tile_id, block_id, is_dac) = converters[0] + + if not self._rfdc_ctrl.set_if(tile_id, block_id, is_dac, freq): + raise RuntimeError("Error setting RFDC IF Frequency") + return self._rfdc_ctrl.get_nco_freq(tile_id, block_id, is_dac) + + def rfdc_get_nco_freq(self, direction, slot_id, channel): + """ + Gets the RFDC NCO Frequency for the specified channel + """ + converters = self._find_converters(slot_id, direction, channel) + assert len(converters) == 1 + (tile_id, block_id, is_dac) = converters[0] + + return self._rfdc_ctrl.get_nco_freq(tile_id, block_id, is_dac) + + ### ADC cal ############################################################### + def set_cal_frozen(self, frozen, slot_id, channel): + """ + Set the freeze state for the ADC cal blocks + + Usage: + > set_cal_frozen <frozen> <slot_id> <channel> + + <frozen> should be 0 to unfreeze the calibration blocks or 1 to freeze them. + """ + for tile_id, block_id, _ in self._find_converters(slot_id, "rx", channel): + self._rfdc_ctrl.set_cal_frozen(tile_id, block_id, frozen) + + def get_cal_frozen(self, slot_id, channel): + """ + Get the freeze states for each ADC cal block in the channel + + Usage: + > get_cal_frozen <slot_id> <channel> + """ + return [ + 1 if self._rfdc_ctrl.get_cal_frozen(tile_id, block_id) else 0 + for tile_id, block_id, is_dac in self._find_converters(slot_id, "rx", channel) + ] + + def set_cal_coefs(self, channel, slot_id, cal_block, coefs): + """ + Manually override calibration block coefficients. You probably don't need to use this. + """ + self.log.trace( + "Setting ADC cal coefficients for channel={} slot_id={} cal_block={}".format( + channel, slot_id, cal_block)) + for tile_id, block_id, _ in self._find_converters(slot_id, "rx", channel): + self._rfdc_ctrl.set_adc_cal_coefficients( + tile_id, block_id, cal_block, ast.literal_eval(coefs)) + + def get_cal_coefs(self, channel, slot_id, cal_block): + """ + Manually retrieve raw coefficients for the ADC calibration blocks. + + Usage: + > get_cal_coefs <channel, 0-1> <slot_id, 0-1> <cal_block, 0-3> + e.g. + > get_cal_coefs 0 1 3 + Retrieves the coefficients for the TSCB block on channel 0 of DB 1. + + Valid values for cal_block are: + 0 - OCB1 (Unaffected by cal freeze) + 1 - OCB2 (Unaffected by cal freeze) + 2 - GCB + 3 - TSCB + """ + self.log.trace( + "Getting ADC cal coefficients for channel={} slot_id={} cal_block={}".format( + channel, slot_id, cal_block)) + result = [] + for tile_id, block_id, _ in self._find_converters(slot_id, "rx", channel): + result.append(self._rfdc_ctrl.get_adc_cal_coefficients(tile_id, block_id, cal_block)) + return result + + ### DAC mux + def set_dac_mux_data(self, i_val, q_val): + """ + Sets the data which is muxed into the DACs when the DAC mux is enabled + + Usage: + > set_dac_mux_data <I> <Q> + e.g. + > set_dac_mux_data 123 456 + """ + self._rfdc_regs.set_cal_data(i_val, q_val) + + def set_dac_mux_enable(self, channel, enable): + """ + Sets whether the DAC mux is enabled for a given channel + + Usage: + > set_dac_mux_enable <channel, 0-3> <enable, 1=enabled> + e.g. + > set_dac_mux_enable 1 0 + """ + self._rfdc_regs.set_cal_enable(channel, bool(enable)) + + ### ADC thresholds + def setup_threshold(self, slot_id, channel, threshold_idx, mode, delay, under, over): + """ + Configure the given ADC threshold block. + + Usage: + > setup_threshold <slot_id> <channel> <threshold_idx> <mode> <delay> <under> <over> + + slot_id: Slot ID to configure, 0 or 1 + channel: Channel on the slot to configure, 0 or 1 + threshold_idx: Threshold block index, 0 or 1 + mode: Mode to configure, one of ["sticky_over", "sticky_under", "hysteresis"] + delay: In hysteresis mode, number of samples before clearing flag. + under: 0-16384, ADC codes to set the "under" threshold to + over: 0-16384, ADC codes to set the "over" threshold to + """ + for tile_id, block_id, _ in self._find_converters(slot_id, "rx", channel): + THRESHOLDS = { + 0: lib.rfdc.threshold_id_options.THRESHOLD_0, + 1: lib.rfdc.threshold_id_options.THRESHOLD_1, + } + MODES = { + "sticky_over": lib.rfdc.threshold_mode_options.TRSHD_STICKY_OVER, + "sticky_under": lib.rfdc.threshold_mode_options.TRSHD_STICKY_UNDER, + "hysteresis": lib.rfdc.threshold_mode_options.TRSHD_HYSTERESIS, + } + if mode not in MODES: + raise RuntimeError( + f"Mode {mode} is not one of the allowable modes {list(MODES.keys())}") + if threshold_idx not in THRESHOLDS: + raise RuntimeError("threshold_idx must be 0 or 1") + delay = int(delay) + under = int(under) + over = int(over) + assert 0 <= under <= 16383 + assert 0 <= over <= 16383 + self._rfdc_ctrl.set_threshold_settings( + tile_id, block_id, + lib.rfdc.threshold_id_options.THRESHOLD_0, + MODES[mode], + delay, + under, + over) + + def get_threshold_status(self, slot_id, channel, threshold_idx): + """ + Read the threshold status bit for the given threshold block from the device. + + Usage: + > get_threshold_status <slot_id> <channel> <threshold_idx> + e.g. + > get_threshold_status 0 1 0 + """ + return self._rfdc_regs.get_threshold_status(slot_id, channel, threshold_idx) != 0 + + + ########################################################################### + # Private helpers (note: x4xx_db_iface calls into those) + ########################################################################### + def _set_interpolation_decimation(self, tile, block, is_dac, factor): + """ + Set the provided interpolation/decimation factor to the + specified ADC/DAC tile, block + + Only gets called from set_reset_rfdc(). + """ + # Map the interpolation/decimation factor to fabric words. + # Keys: is_dac (False -> ADC, True -> DAC) and factor + # Disable FIFO + self._rfdc_ctrl.set_data_fifo_state(tile, is_dac, False) + # Define fabric rate based on given factor. + fab_words = FABRIC_WORDS_ARRAY[is_dac].get(int(factor)) + if fab_words == -1: + raise RuntimeError('Unsupported dec/int factor in RFDC') + # Define dec/int constant based on integer factor + if factor == 0: + int_dec = lib.rfdc.interp_decim_options.INTERP_DECIM_OFF + elif factor == 1: + int_dec = lib.rfdc.interp_decim_options.INTERP_DECIM_1X + elif factor == 2: + int_dec = lib.rfdc.interp_decim_options.INTERP_DECIM_2X + elif factor == 4: + int_dec = lib.rfdc.interp_decim_options.INTERP_DECIM_4X + elif factor == 8: + int_dec = lib.rfdc.interp_decim_options.INTERP_DECIM_8X + else: + raise RuntimeError('Unsupported dec/int factor in RFDC') + # Update tile, block settings... + self.log.debug( + "Setting %s for %s tile %d, block %d to %dx", + ('interpolation' if is_dac else 'decimation'), + 'DAC' if is_dac else 'ADC', tile, block, factor) + if is_dac: + # Set interpolation + self._rfdc_ctrl.set_interpolation_factor(tile, block, int_dec) + self.log.trace( + " interpolation: %s", + self._rfdc_ctrl.get_interpolation_factor(tile, block).name) + # Set fabric write rate + self._rfdc_ctrl.set_data_write_rate(tile, block, fab_words) + self.log.trace( + " Read words: %d", + self._rfdc_ctrl.get_data_write_rate(tile, block, True)) + else: # ADC + # Set decimation + self._rfdc_ctrl.set_decimation_factor(tile, block, int_dec) + self.log.trace( + " Decimation: %s", + self._rfdc_ctrl.get_decimation_factor(tile, block).name) + # Set fabric read rate + self._rfdc_ctrl.set_data_read_rate(tile, block, fab_words) + self.log.trace( + " Read words: %d", + self._rfdc_ctrl.get_data_read_rate(tile, block, False)) + # Clear interrupts + self._rfdc_ctrl.clear_data_fifo_interrupts(tile, block, is_dac) + # Enable FIFO + self._rfdc_ctrl.set_data_fifo_state(tile, is_dac, True) + + + def _find_converters(self, slot, direction, channel): + """ + Returns a list of (tile_id, block_id, is_dac) tuples describing + the data converters associated with a given channel and direction. + """ + if direction not in ('rx', 'tx', 'both'): + self.log.error('Invalid direction "{}". Cannot find ' + 'associated data converters'.format(direction)) + raise RuntimeError('Invalid direction "{}". Cannot find ' + 'associated data converters'.format(direction)) + if str(channel) not in ('0', '1', 'both'): + self.log.error('Invalid channel "{}". Cannot find ' + 'associated data converters'.format(channel)) + raise RuntimeError('Invalid channel "{}". Cannot find ' + 'associated data converters'.format(channel)) + data_converters = [] + rfdc_map = self.RFDC_DB_MAP[slot] + + if direction in ('rx', 'both'): + if str(channel) == '0' or str(channel) == 'both': + (tile_id, block_id) = rfdc_map['adc'][0] + data_converters.append((tile_id, block_id, False)) + if str(channel) == '1' or str(channel) == 'both': + (tile_id, block_id) = rfdc_map['adc'][1] + data_converters.append((tile_id, block_id, False)) + if direction in ('tx', 'both'): + if str(channel) == '0' or str(channel) == 'both': + (tile_id, block_id) = rfdc_map['dac'][0] + data_converters.append((tile_id, block_id, True)) + if str(channel) == '1' or str(channel) == 'both': + (tile_id, block_id) = rfdc_map['dac'][1] + data_converters.append((tile_id, block_id, True)) + return data_converters diff --git a/mpm/python/usrp_mpm/periph_manager/x4xx_rfdc_regs.py b/mpm/python/usrp_mpm/periph_manager/x4xx_rfdc_regs.py new file mode 100644 index 000000000..95f3cc007 --- /dev/null +++ b/mpm/python/usrp_mpm/periph_manager/x4xx_rfdc_regs.py @@ -0,0 +1,263 @@ +# +# Copyright 2021 Ettus Research, a National Instruments Brand +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +""" +X4xx RFDC register control +""" + +import time +from usrp_mpm.sys_utils.uio import UIO + +class RfdcRegsControl: + """ + Control the FPGA RFDC registers external to the XRFdc API + """ + # pylint: disable=bad-whitespace + IQ_SWAP_OFFSET = 0x10000 + MMCM_RESET_BASE_OFFSET = 0x11000 + RF_RESET_CONTROL_OFFSET = 0x12000 + RF_RESET_STATUS_OFFSET = 0x12008 + RF_STATUS_OFFSET = 0x13000 + FABRIC_DSP_INFO_OFFSET = 0x13008 + CAL_DATA_OFFSET = 0x14000 + CAL_ENABLE_OFFSET = 0x14008 + THRESHOLD_STATUS_OFFSET = 0x15000 + RF_PLL_CONTROL_OFFSET = 0x16000 + RF_PLL_STATUS_OFFSET = 0x16008 + # pylint: enable=bad-whitespace + + def __init__(self, label, log): + self.log = log.getChild("RfdcRegs") + self.regs = UIO( + label=label, + read_only=False + ) + self.poke32 = self.regs.poke32 + self.peek32 = self.regs.peek32 + + # Index corresponds to dboard number. + self._converter_chains_in_reset = True + + def get_threshold_status(self, slot_id, channel, threshold_idx): + """ + Retrieves the status bit for the given threshold block + """ + BITMASKS = { + (0, 0, 0): 0x04, + (0, 0, 1): 0x08, + (0, 1, 0): 0x01, + (0, 1, 1): 0x02, + (1, 0, 0): 0x400, + (1, 0, 1): 0x800, + (1, 1, 0): 0x100, + (1, 1, 1): 0x200, + } + assert (slot_id, channel, threshold_idx) in BITMASKS + status = self.peek(self.THRESHOLD_STATUS_OFFSET) + status_bool = (status & BITMASKS[(slot_id, channel, threshold_idx)]) != 0 + return 1 if status_bool else 0 + + def set_cal_data(self, i, q): + assert 0 <= i < 2**16 + assert 0 <= q < 2**16 + self.poke(self.CAL_DATA_OFFSET, (q << 16) | i) + + def set_cal_enable(self, channel, enable): + assert 0 <= channel <= 3 + assert enable in [False, True] + en = self.peek(self.CAL_ENABLE_OFFSET) + bit_offsets = { + 0: 0, + 1: 1, + 2: 4, + 3: 5, + } + en_mask = 1 << bit_offsets[channel] + en = en & ~en_mask + self.poke(self.CAL_ENABLE_OFFSET, en | (en_mask if enable else 0)) + + def enable_iq_swap(self, enable, db_id, block_id, is_dac): + iq_swap_bit = (int(is_dac) * 8) + (db_id * 4) + block_id + + # Write IQ swap bit with a mask + reg_val = self.peek(self.IQ_SWAP_OFFSET) + reg_val = (reg_val & ~(1 << iq_swap_bit)) \ + | (enable << iq_swap_bit) + self.poke(self.IQ_SWAP_OFFSET, reg_val) + + def set_reset_mmcm(self, reset=True): + if reset: + # Put the MMCM in reset (active low) + self.poke(self.MMCM_RESET_BASE_OFFSET, 0) + else: + # Take the MMCM out of reset + self.poke(self.MMCM_RESET_BASE_OFFSET, 1) + + def wait_for_mmcm_locked(self, timeout=0.001): + """ + Wait for MMCM to come to a stable locked state. + The datasheet specifies a 100us max lock time + """ + DATA_CLK_PLL_LOCKED = 1 << 20 + + POLL_SLEEP = 0.0002 + for _ in range(int(timeout / POLL_SLEEP)): + time.sleep(POLL_SLEEP) + status = self.peek(self.RF_PLL_STATUS_OFFSET) + if status & DATA_CLK_PLL_LOCKED: + self.log.trace("RF MMCM lock detected.") + return + self.log.error("MMCM failed to lock in the expected time.") + raise RuntimeError("MMCM failed to lock within the expected time.") + + def set_gated_clock_enables(self, value=True): + """ + Controls the clock enable for data_clk and + data_clk_2x + """ + ENABLE_DATA_CLK = 1 + ENABLE_DATA_CLK_2X = 1 << 4 + ENABLE_RF_CLK = 1 << 8 + ENABLE_RF_CLK_2X = 1 << 12 + if value: + # Enable buffers gating the clocks + self.poke(self.RF_PLL_CONTROL_OFFSET, + ENABLE_DATA_CLK | + ENABLE_DATA_CLK_2X | + ENABLE_RF_CLK | + ENABLE_RF_CLK_2X + ) + else: + # Disable clock buffers to have clocks gated. + self.poke(self.RF_PLL_CONTROL_OFFSET, 0) + + def get_fabric_dsp_info(self, dboard): + """ + Read the DSP information register and returns the + DSP bandwidth, rx channel count and tx channel count + """ + # Offsets + DSP_BW = 0 + 16*dboard + DSP_RX_CNT = 12 + 16*dboard + DSP_TX_CNT = 14 + 16*dboard + # Masks + DSP_BW_MSK = 0xFFF + DSP_RX_CNT_MSK = 0x3 + DSP_TX_CNT_MSK = 0x3 + + dsp_info = self.peek(self.FABRIC_DSP_INFO_OFFSET) + self.log.trace("Fabric DSP for dboard %d...", dboard) + dsp_bw = (dsp_info >> DSP_BW) & DSP_BW_MSK + self.log.trace(" Bandwidth (MHz): %d", dsp_bw) + dsp_rx_cnt = (dsp_info >> DSP_RX_CNT) & DSP_RX_CNT_MSK + self.log.trace(" Rx channel count: %d", dsp_rx_cnt) + dsp_tx_cnt = (dsp_info >> DSP_TX_CNT) & DSP_TX_CNT_MSK + self.log.trace(" Tx channel count: %d", dsp_tx_cnt) + + return [dsp_bw, dsp_rx_cnt, dsp_tx_cnt] + + def get_rfdc_resampling_factor(self, dboard): + """ + Returns the appropriate decimation/interpolation factor to set in the RFDC. + """ + # DSP vs. RFDC decimation/interpolation dictionary + # Key: bandwidth in MHz + # Value: (RFDC resampling factor, is Half-band resampling used?) + RFDC_RESAMPLING_FACTOR = { + 100: (8, False), # 100 MHz BW requires 8x RFDC resampling + 200: (2, True), # 200 MHz BW requires 2x RFDC resampling + # (400 MHz RFDC DSP used w/ half-band resampling) + 400: (2, False) # 400 MHz BW requires 2x RFDC resampling + } + dsp_bw, _, _ = self.get_fabric_dsp_info(dboard) + # When no RF fabric DSP is present (dsp_bw = 0), MPM should + # simply use the default RFDC resampling factor (400 MHz). + if dsp_bw in RFDC_RESAMPLING_FACTOR: + rfdc_resampling_factor, halfband = RFDC_RESAMPLING_FACTOR[dsp_bw] + else: + rfdc_resampling_factor, halfband = RFDC_RESAMPLING_FACTOR[400] + self.log.trace(" Using default resampling!") + self.log.trace(" RFDC resampling: %d", rfdc_resampling_factor) + return (rfdc_resampling_factor, halfband) + + def set_reset_adc_dac_chains(self, reset=True): + """ Resets or enables the ADC and DAC chain for the given dboard """ + + def _wait_for_done(done_bit, timeout=5): + """ + Wait for the specified sequence done bit when resetting or + enabling an ADC or DAC chain. Throws an error on timeout. + """ + status = self.peek(self.RF_RESET_STATUS_OFFSET) + if (status & done_bit): + return + for _ in range(0, timeout): + time.sleep(0.001) # 1 ms + status = self.peek(self.RF_RESET_STATUS_OFFSET) + if (status & done_bit): + return + self.log.error("Timeout while resetting or enabling ADC/DAC chains.") + raise RuntimeError("Timeout while resetting or enabling ADC/DAC chains.") + + # CONTROL OFFSET + ADC_RESET = 1 << 4 + DAC_RESET = 1 << 8 + # STATUS OFFSET + ADC_SEQ_DONE = 1 << 7 + DAC_SEQ_DONE = 1 << 11 + + if reset: + if self._converter_chains_in_reset: + self.log.debug('Converters are already in reset. ' + 'The reset bit will NOT be toggled.') + return + # Reset the ADC and DAC chains + self.log.trace('Resetting ADC chain') + self.poke(self.RF_RESET_CONTROL_OFFSET, ADC_RESET) + _wait_for_done(ADC_SEQ_DONE) + self.poke(self.RF_RESET_CONTROL_OFFSET, 0x0) + + self.log.trace('Resetting DAC chain') + self.poke(self.RF_RESET_CONTROL_OFFSET, DAC_RESET) + _wait_for_done(DAC_SEQ_DONE) + self.poke(self.RF_RESET_CONTROL_OFFSET, 0x0) + + self._converter_chains_in_reset = True + else: # enable + self._converter_chains_in_reset = False + + def log_status(self): + status = self.peek(self.RF_STATUS_OFFSET) + self.log.debug("Daughterboard 0") + self.log.debug(" @RFDC") + self.log.debug(" DAC(1:0) TREADY : {:02b}".format((status >> 0) & 0x3)) + self.log.debug(" DAC(1:0) TVALID : {:02b}".format((status >> 2) & 0x3)) + self.log.debug(" ADC(1:0) I TREADY : {:02b}".format((status >> 6) & 0x3)) + self.log.debug(" ADC(1:0) I TVALID : {:02b}".format((status >> 10) & 0x3)) + self.log.debug(" ADC(1:0) Q TREADY : {:02b}".format((status >> 4) & 0x3)) + self.log.debug(" ADC(1:0) Q TVALID : {:02b}".format((status >> 8) & 0x3)) + self.log.debug(" @USER") + self.log.debug(" ADC(1:0) OUT TVALID: {:02b}".format((status >> 12) & 0x3)) + self.log.debug(" ADC(1:0) OUT TREADY: {:02b}".format((status >> 14) & 0x3)) + self.log.debug("Daughterboard 1") + self.log.debug(" @RFDC") + self.log.debug(" DAC(1:0) TREADY : {:02b}".format((status >> 16) & 0x3)) + self.log.debug(" DAC(1:0) TVALID : {:02b}".format((status >> 18) & 0x3)) + self.log.debug(" ADC(1:0) I TREADY : {:02b}".format((status >> 22) & 0x3)) + self.log.debug(" ADC(1:0) I TVALID : {:02b}".format((status >> 26) & 0x3)) + self.log.debug(" ADC(1:0) Q TREADY : {:02b}".format((status >> 20) & 0x3)) + self.log.debug(" ADC(1:0) Q TVALID : {:02b}".format((status >> 24) & 0x3)) + self.log.debug(" @USER") + self.log.debug(" ADC(1:0) OUT TVALID: {:02b}".format((status >> 28) & 0x3)) + self.log.debug(" ADC(1:0) OUT TREADY: {:02b}".format((status >> 30) & 0x3)) + + def poke(self, addr, val): + with self.regs: + self.regs.poke32(addr, val) + + def peek(self, addr): + with self.regs: + result = self.regs.peek32(addr) + return result diff --git a/mpm/python/usrp_mpm/periph_manager/x4xx_sample_pll.py b/mpm/python/usrp_mpm/periph_manager/x4xx_sample_pll.py new file mode 100644 index 000000000..3c3040d55 --- /dev/null +++ b/mpm/python/usrp_mpm/periph_manager/x4xx_sample_pll.py @@ -0,0 +1,315 @@ +# +# Copyright 2019 Ettus Research, a National Instruments Brand +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +""" +LMK04832 driver for use with X4xx +""" + +from usrp_mpm.chips import LMK04832 +from usrp_mpm.sys_utils.gpio import Gpio + +class LMK04832X4xx(LMK04832): + """ + X4xx-specific subclass of the Sample Clock PLL LMK04832 controls. + """ + def __init__(self, pll_regs_iface, log=None): + LMK04832.__init__(self, pll_regs_iface, log) + self._output_freq = None + self._is_legacy_mode = None + + self._sclk_pll_reset = Gpio('SAMPLE-CLOCK-PLL-RESET', Gpio.OUTPUT, 0) + self._sclk_pll_select = Gpio('SAMPLE-CLOCK-PLL-VCXO-SELECT', Gpio.OUTPUT, 0) + + @property + def is_legacy_mode(self): + if self._is_legacy_mode is None: + self.log.error('The Sample PLL was never configured before ' + 'checking for legacy mode!') + raise RuntimeError('The Sample PLL was never configured before ' + 'checking for legacy mode!') + return self._is_legacy_mode + + @property + def output_freq(self): + if self._output_freq is None: + self.log.error('The Sample PLL was never configured before ' + 'checking the output frequency!') + raise RuntimeError('The Sample PLL was never configured before ' + 'checking the output frequency!') + return self._output_freq + + def init(self): + """ + Perform a soft reset, configure SPI readback, and verify chip ID + """ + self.reset(False, hard=True) + self.reset(True) + self.reset(False) + if not self.verify_chip_id(): + raise Exception("unable to locate LMK04832!") + + def reset(self, value=True, hard=False): + """ + Perform a hard reset from the GPIO pins or a soft reset from the LMK register + """ + if hard: + self._sclk_pll_reset.set(value) + else: + self.soft_reset(value) + + if not value: + # Enable 4-wire spi readback after a reset. 4-wire SPI is disabled + # by default after a reset of the LMK, but is required to perform + # SPI reads on the x4xx. + self.enable_4wire_spi() + + def enable_4wire_spi(self): + """ Enable 4-wire SPI readback from the CLKin_SEL0 pin """ + self.poke8(0x148, 0x33) + self.enable_3wire_spi = False + + def set_vcxo(self, source_freq): + """ + Selects either the 100e6 MHz or 122.88e6 MHz VCXO for the PLL1 loop of the LMK04832. + """ + if source_freq == 100e6: + source_index = 0 + elif source_freq == 122.88e6: + source_index = 1 + else: + self.log.warning( + 'Selected VCXO source of {:g} is not a valid selection' + .format(source_freq)) + return + self.log.trace( + 'Selected VCXO source of {:g}' + .format(source_freq)) + self._sclk_pll_select.set(source_index) + + def get_status(self): + """ + Returns PLL lock status + """ + pll1_status = self.check_plls_locked(pll='PLL1') + pll2_status = self.check_plls_locked(pll='PLL2') + return {'PLL1 lock': pll1_status, + 'PLL2 lock': pll2_status} + + def config(self, output_freq, brc_freq, is_legacy_mode=False): + """ + Configures the LMK04832 to generate the desired output_freq + """ + def calculate_vcxo_freq(output_freq): + """ + Returns the vcxo frequency based on the desired output frequency + """ + return {2.94912e9: 122.88e6, 3e9: 100e6, 3.072e9: 122.88e6}[output_freq] + def calculate_pll1_n_div(output_freq): + """ + Returns the PLL1 N divider value based on the desired output frequency + """ + return {2.94912e9: 64, 3e9: 50, 3.072e9: 64}[output_freq] + def calculate_pll2_n_div(output_freq): + """ + Returns the PLL2 N divider value based on the desired output frequency + """ + return {2.94912e9: 12, 3e9: 10, 3.072e9: 5}[output_freq] + def calculate_pll2_pre(output_freq): + """ + Returns the PLL2 prescaler value based on the desired output frequency + """ + return {2.94912e9: 2, 3e9: 3, 3.072e9: 5}[output_freq] + def calculate_n_cal_div(output_freq): + """ + Returns the PLL2 N cal value based on the desired output frequency + """ + return {2.94912e9: 12, 3e9: 10, 3.072e9: 5}[output_freq] + def calculate_sysref_div(output_freq): + """ + Returns the SYSREF divider value based on the desired output frequency + """ + return {2.94912e9: 1152, 3e9: 1200, 3.072e9: 1200}[output_freq] + def calculate_clk_in_0_r_div(output_freq, brc_freq): + """ + Returns the CLKin0 R divider value based on the desired output frequency + and current base reference clock frequency + """ + pfd1 = {2.94912e9: 40e3, 3e9: 50e3, 3.072e9: 40e3}[output_freq] + return int(brc_freq / pfd1) + + if output_freq not in (2.94912e9, 3e9, 3.072e9): + # A failure to config the SPLL could lead to an invalid state for + # downstream clocks, so throw here to alert the caller. + raise RuntimeError( + 'Selected output_freq of {:g} is not a valid selection' + .format(output_freq)) + + self._is_legacy_mode = is_legacy_mode + self._output_freq = output_freq + + self.log.trace( + f"Configuring SPLL to output frequency of {output_freq} Hz, used " + f"BRC frquency is {brc_freq} Hz, legacy mode is {is_legacy_mode}") + + self.set_vcxo(calculate_vcxo_freq(output_freq)) + + # Clear hard reset and trigger soft reset + self.reset(False, hard=True) + self.reset(True, hard=False) + self.reset(False, hard=False) + + prc_divider = 0x3C if is_legacy_mode else 0x30 + + # CLKout Config + self.pokes8(( + (0x0100, 0x01), + (0x0101, 0x0A), + (0x0102, 0x70), + (0x0103, 0x44), + (0x0104, 0x10), + (0x0105, 0x00), + (0x0106, 0x00), + (0x0107, 0x55), + (0x0108, 0x01), + (0x0109, 0x0A), + (0x010A, 0x70), + (0x010B, 0x44), + (0x010C, 0x10), + (0x010D, 0x00), + (0x010E, 0x00), + (0x010F, 0x55), + (0x0110, prc_divider), + (0x0111, 0x0A), + (0x0112, 0x60), + (0x0113, 0x40), + (0x0114, 0x10), + (0x0115, 0x00), + (0x0116, 0x00), + (0x0117, 0x44), + (0x0118, prc_divider), + (0x0119, 0x0A), + (0x011A, 0x60), + (0x011B, 0x40), + (0x011C, 0x10), + (0x011D, 0x00), + (0x011E, 0x00), + (0x011F, 0x44), + (0x0120, prc_divider), + (0x0121, 0x0A), + (0x0122, 0x60), + (0x0123, 0x40), + (0x0124, 0x20), + (0x0125, 0x00), + (0x0126, 0x00), + (0x0127, 0x44), + (0x0128, 0x01), + (0x0129, 0x0A), + (0x012A, 0x60), + (0x012B, 0x60), + (0x012C, 0x20), + (0x012D, 0x00), + (0x012E, 0x00), + (0x012F, 0x44), + (0x0130, 0x01), + (0x0131, 0x0A), + (0x0132, 0x70), + (0x0133, 0x44), + (0x0134, 0x10), + (0x0135, 0x00), + (0x0136, 0x00), + (0x0137, 0x55), + )) + + # PLL Config + sysref_div = calculate_sysref_div(output_freq) + clk_in_0_r_div = calculate_clk_in_0_r_div(output_freq, brc_freq) + pll1_n_div = calculate_pll1_n_div(output_freq) + prescaler = self.pll2_pre_to_reg(calculate_pll2_pre(output_freq)) + pll2_n_cal_div = calculate_n_cal_div(output_freq) + pll2_n_div = calculate_pll2_n_div(output_freq) + self.pokes8(( + (0x0138, 0x20), + (0x0139, 0x00), # Set SysRef source to 'Normal SYNC' as we initially use the sync signal to synchronize dividers + (0x013A, (sysref_div & 0x1F00) >> 8), # SYSREF Divide [12:8] + (0x013B, (sysref_div & 0x00FF) >> 0), # SYSREF Divide [7:0] + (0x013C, 0x00), # set sysref delay value + (0x013D, 0x20), # shift SYSREF with respect to falling edge of data clock + (0x013E, 0x03), # set number of SYSREF pulse to 8(Default) + (0x013F, 0x0F), # PLL1_NCLK_MUX = Feedback mux, FB_MUX = External, FB_MUX_EN = enabled + (0x0140, 0x00), # All power down controls set to false. + (0x0141, 0x00), # Disable dynamic digital delay. + (0x0142, 0x00), # Set dynamic digtial delay step count to 0. + (0x0143, 0x81), # Enable SYNC pin, disable sync functionality, SYSREF_CLR='0, SYNC is level sensitive. + (0x0144, 0x00), # Allow SYNC to synchronize all SysRef and clock outputs + (0x0145, 0x10), # Disable PLL1 R divider SYNC, use SYNC pin for PLL1 R divider SYNC, disable PLL2 R divider SYNC + (0x0146, 0x00), # CLKIN0/1 type = Bipolar, disable CLKin_sel pin, disable both CLKIn source for auto-switching. + (0x0147, 0x06), # ClkIn0_Demux= PLL1, CLKIn1-Demux=Feedback mux (need for 0-delay mode) + (0x0148, 0x33), # CLKIn_Sel0 = SPI readback with output set to push-pull + (0x0149, 0x02), # Set SPI readback ouput to open drain (needed for 4-wire) + (0x014A, 0x00), # Set RESET pin as input + (0x014B, 0x02), # Default + (0x014C, 0x00), # Default + (0x014D, 0x00), # Default + (0x014E, 0xC0), # Default + (0x014F, 0x7F), # Default + (0x0150, 0x00), # Default and disable holdover + (0x0151, 0x02), # Default + (0x0152, 0x00), # Default + (0x0153, (clk_in_0_r_div & 0x3F00) >> 8), # CLKin0_R divider [13:8], default = 0 + (0x0154, (clk_in_0_r_div & 0x00FF) >> 0), # CLKin0_R divider [7:0], default = d120 + (0x0155, 0x00), # Set CLKin1 R divider to 1 + (0x0156, 0x01), # Set CLKin1 R divider to 1 + (0x0157, 0x00), # Set CLKin2 R divider to 1 + (0x0158, 0x01), # Set CLKin2 R divider to 1 + (0x0159, (pll1_n_div & 0x3F00) >> 8), # PLL1 N divider [13:8], default = 0 + (0x015A, (pll1_n_div & 0x00FF) >> 0), # PLL1 N divider [7:0], default = d120 + (0x015B, 0xCF), # Set PLL1 window size to 43ns, PLL1 CP ON, negative polarity, CP gain is 1.55 mA. + (0x015C, 0x20), # Pll1 lock detect count is 8192 cycles (default) + (0x015D, 0x00), # Pll1 lock detect count is 8192 cycles (default) + (0x015E, 0x1E), # Default holdover relative time between PLL1 R and PLL1 N divider + (0x015F, 0x1B), # PLL1 and PLL2 locked status in Status_LD1 pin. Status_LD1 pin is ouput (push-pull) + (0x0160, 0x00), # PLL2 R divider is 1 + (0x0161, 0x01), # PLL2 R divider is 1 + (0x0162, prescaler), # PLL2 prescaler; OSCin freq; Lower nibble must be 0x4!!! + (0x0163, (pll2_n_cal_div & 0x030000) >> 16), # PLL2 N Cal [17:16] + (0x0164, (pll2_n_cal_div & 0x00FF00) >> 8), # PLL2 N Cal [15:8] + (0x0165, (pll2_n_cal_div & 0x0000FF) >> 0), # PLL2 N Cal [7:0] + (0x0169, 0x59), # Write this val after x165. PLL2 CP gain is 3.2 mA, PLL2 window is 1.8 ns + (0x016A, 0x20), # PLL2 lock detect count is 8192 cycles (default) + (0x016B, 0x00), # PLL2 lock detect count is 8192 cycles (default) + (0x016E, 0x13), # Stautus_LD2 pin not used. Don't care about this register + (0x0173, 0x10), # PLL2 prescaler and PLL2 are enabled. + (0x0177, 0x00), # PLL1 R divider not in reset + (0x0166, (pll2_n_div & 0x030000) >> 16), # PLL2 N[17:16] + (0x0167, (pll2_n_div & 0x00FF00) >> 8), # PLL2 N[15:8] + (0x0168, (pll2_n_div & 0x0000FF) >> 0), # PLL2 N[7:0] + )) + + # Synchronize Output and SYSREF Dividers + self.pokes8(( + (0x0143, 0x91), + (0x0143, 0xB1), + (0x0143, 0x91), + (0x0144, 0xFF), + (0x0143, 0x11), + (0x0139, 0x12), + (0x0143, 0x31), + )) + + # Check for Lock + # PLL2 should lock first and be relatively fast (300 us) + if self.wait_for_pll_lock('PLL2', timeout=5): + self.log.trace("PLL2 is locked after SPLL config.") + else: + self.log.error('Sample Clock PLL2 failed to lock!') + raise RuntimeError('Sample Clock PLL2 failed to lock! ' + 'Check the logs for details.') + # PLL1 may take up to 2 seconds to lock + if self.wait_for_pll_lock('PLL1', timeout=2000): + self.log.trace("PLL1 is locked after SPLL config.") + else: + self.log.error('Sample Clock PLL1 failed to lock!') + raise RuntimeError('Sample Clock PLL1 failed to lock! ' + 'Check the logs for details.') diff --git a/mpm/python/usrp_mpm/periph_manager/x4xx_update_cpld.py b/mpm/python/usrp_mpm/periph_manager/x4xx_update_cpld.py new file mode 100644 index 000000000..8920b6941 --- /dev/null +++ b/mpm/python/usrp_mpm/periph_manager/x4xx_update_cpld.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python3 +# +# Copyright 2019 Ettus Research, a National Instruments Brand +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +""" +Update the CPLD image for the X4xx +""" + +import sys +import os +import argparse +import subprocess +import pyudev +from usrp_mpm.mpmlog import get_logger +from usrp_mpm.sys_utils.sysfs_gpio import GPIOBank +from usrp_mpm.sys_utils.udev import dt_symbol_get_spidev +from usrp_mpm.periph_manager.x4xx_mb_cpld import MboardCPLD +from usrp_mpm.periph_manager.x4xx import x4xx +from usrp_mpm.chips.max10_cpld_flash_ctrl import Max10CpldFlashCtrl + +OPENOCD_DIR = "/usr/share/openocd/scripts" +CONFIGS = { + 'axi_bitq' : { + 'files' : ["fpga/altera-10m50.cfg"], + 'cmd' : ["interface axi_bitq; axi_bitq_config %u %u; adapter_khz %u", + "init; svf -tap 10m50.tap %s -progress -quiet;exit"] + } +} + +AXI_BITQ_ADAPTER_SPEED = 5000 +AXI_BITQ_BUS_CLK = 40000000 + +def check_openocd_files(files, logger=None): + """ + Check if all file required by OpenOCD exist + :param logger: logger object + """ + for ocd_file in files: + if not os.path.exists(os.path.join(OPENOCD_DIR, ocd_file)): + if logger is not None: + logger.error("Missing file %s" % os.path.join(OPENOCD_DIR, ocd_file)) + return False + return True + +def check_fpga_state(which=0): + """ + Check if the FPGA is operational + :param which: the FPGA to check + """ + logger = get_logger('update_cpld') + try: + context = pyudev.Context() + fpga_mgrs = list(context.list_devices(subsystem="fpga_manager")) + if fpga_mgrs: + state = fpga_mgrs[which].attributes.asstring('state') + logger.trace("FPGA State: {}".format(state)) + return state == "operating" + return False + except OSError as ex: + logger.error("Error while checking FPGA status: {}".format(ex)) + return False + +def find_axi_bitq_uio(): + """ + Find the AXI Bitq UIO device + """ + label = 'jtag-0' + + logger = get_logger('update_cpld') + + try: + context = pyudev.Context() + for uio in context.list_devices(subsystem="uio"): + uio_label = uio.attributes.asstring('maps/map0/name') + logger.trace("UIO label: {}, match: {} number: {}".format( + uio_label, uio_label == label, uio.sys_number)) + if uio_label == label: + return int(uio.sys_number) + return None + except OSError as ex: + logger.error("Error while looking for axi_bitq uio nodes: {}".format(ex)) + return None + +def get_gpio_controls(): + """ + Instantiates an object to control JTAG related GPIO pins + Bank 3 - Pin 0: Allows toggle of JTAG Enable and additional signals + Bank 3 - Pin 1: JTAG Enable signal to the CPLD + """ + # Bank 3 starts at pin 78 + offset = 78 + mask = 0x03 + ddr = 0x03 + return GPIOBank({'label': 'zynqmp_gpio'}, offset, mask, ddr) + +def enable_jtag_gpio(gpios, disable=False): + """ + Toggle JTAG Enable line to the CPLD + """ + CPLD_JTAG_OE_n_pin = 0 + PL_CPLD_JTAGEN_pin = 1 + if not disable: + gpios.set(CPLD_JTAG_OE_n_pin, 0) # CPLD_JTAG_OE_n is active low + gpios.set(PL_CPLD_JTAGEN_pin, 1) # PL_CPLD_JTAGEN is active high + else: + gpios.set(CPLD_JTAG_OE_n_pin, 0) # CPLD_JTAG_OE_n is active low + gpios.set(PL_CPLD_JTAGEN_pin, 0) # PL_CPLD_JTAGEN is active high + +def do_update_cpld(filename, updater_mode): + """ + Carry out update process for the CPLD + :param filename: path (on device) to the new CPLD image + :param updater_mode: the updater method to use- Either flash or legacy + :return: True on success, False otherwise + """ + assert updater_mode in ('legacy', 'flash'), \ + f"Invalid updater method {updater_mode} given" + logger = get_logger('update_cpld') + logger.info("Programming CPLD of mboard with image {} using {} mode" + .format(filename, updater_mode)) + + if not os.path.exists(filename): + logger.error("CPLD image file {} not found".format(filename)) + return False + + if updater_mode == 'legacy': + return jtag_cpld_update(filename, logger) + if updater_mode == 'flash': + cpld_spi_node = dt_symbol_get_spidev('mb_cpld') + regs = MboardCPLD(cpld_spi_node, logger) + reconfig_engine_offset = 0x40 + cpld_min_revision = 0x19100108 + flash_control = Max10CpldFlashCtrl(logger, regs, reconfig_engine_offset, cpld_min_revision) + return flash_control.update(filename) + return False + +def jtag_cpld_update(filename, logger): + """ + Update the MB CPLD via dedicated JTAG lines in the FPGA + Note: To use this update mechanism, a FPGA image with JTAG + lines must be loaded. + """ + if logger is None: + logger = get_logger('update_cpld') + + if not check_fpga_state(): + logger.error("CPLD lines are routed through fabric, " + "FPGA is not programmed, giving up") + return False + + mode = 'axi_bitq' + config = CONFIGS[mode] + + if not filename.endswith('svf'): + logger.warning('The legacy JTAG programming mechanism expects ' + '.svf files. The CPLD file being used may be incorrect.') + + if check_openocd_files(config['files'], logger=logger): + logger.trace("Found required OpenOCD files.") + else: + # check_openocd_files logs errors + return False + + uio_id = find_axi_bitq_uio() + if uio_id is None or uio_id < 0: + logger.error('Failed to find axi_bitq uio devices. '\ + 'Make sure overlays are up to date') + return False + + try: + gpios = get_gpio_controls() + except RuntimeError as ex: + logger.error('Could not open GPIO required for JTAG programming!'\ + ' {}'.format(ex)) + return False + enable_jtag_gpio(gpios) + + cmd = ["openocd", + "-c", config['cmd'][0] % (uio_id, AXI_BITQ_BUS_CLK, AXI_BITQ_ADAPTER_SPEED), + "-f", (config['files'][0]).strip(), + "-c", config['cmd'][1] % filename] + + logger.trace("Update CPLD CMD: {}".format(" ".join(cmd))) + subprocess.call(cmd) + + # Disable JTAG dual-purpose pins to CPLD after reprogramming + enable_jtag_gpio(gpios, disable=True) + + logger.trace("Done programming CPLD...") + return True + + +def main(): + """ + Go, go, go! + """ + def parse_args(): + """Parse the command-line arguments""" + parser = argparse.ArgumentParser(description='Update the CPLD image on the X4xx') + parser.add_argument("--file", help="Filename of CPLD image", + default="/lib/firmware/ni/cpld-x410.rpd") + parser.add_argument("--updater", + help="The image updater method to use, either \"legacy\" or \"flash\"", + default="flash") + parser.add_argument( + '-v', + '--verbose', + help="Increase verbosity level", + action="count", + default=1 + ) + parser.add_argument( + '-q', + '--quiet', + help="Decrease verbosity level", + action="count", + default=0 + ) + return parser.parse_args() + + args = parse_args() + + # We need to make a logger if we're running stand-alone + from usrp_mpm.mpmlog import get_main_logger + log = get_main_logger(log_default_delta=args.verbose-args.quiet) + + return do_update_cpld(args.file, args.updater) + +if __name__ == "__main__": + sys.exit(not main()) |