diff options
author | Brent Stapleton <brent.stapleton@ettus.com> | 2018-04-17 19:33:23 -0700 |
---|---|---|
committer | Brent Stapleton <bstapleton@g.hmc.edu> | 2018-07-18 15:37:27 -0700 |
commit | 300a5e3f6e5e845b4b8d093222e1c51ca4640f79 (patch) | |
tree | 62f8bb7bc7d847b8f32f1fe5b4c9c06ef608080e /mpm/python/usrp_mpm | |
parent | a1d6530ce50ca9590739ffa40464747d3db968eb (diff) | |
download | uhd-300a5e3f6e5e845b4b8d093222e1c51ca4640f79.tar.gz uhd-300a5e3f6e5e845b4b8d093222e1c51ca4640f79.tar.bz2 uhd-300a5e3f6e5e845b4b8d093222e1c51ca4640f79.zip |
mpm: initial commit of E320 code
Co-authored-by: Sugandha Gupta <sugandha.gupta@ettus.com>
Diffstat (limited to 'mpm/python/usrp_mpm')
-rw-r--r-- | mpm/python/usrp_mpm/dboard_manager/CMakeLists.txt | 5 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/dboard_manager/__init__.py | 4 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/dboard_manager/neon.py | 281 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/periph_manager/CMakeLists.txt | 8 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/periph_manager/e320.py | 671 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/periph_manager/e320_periphs.py | 383 |
6 files changed, 1345 insertions, 7 deletions
diff --git a/mpm/python/usrp_mpm/dboard_manager/CMakeLists.txt b/mpm/python/usrp_mpm/dboard_manager/CMakeLists.txt index 6ad2f91f0..64217be07 100644 --- a/mpm/python/usrp_mpm/dboard_manager/CMakeLists.txt +++ b/mpm/python/usrp_mpm/dboard_manager/CMakeLists.txt @@ -1,7 +1,7 @@ # -# Copyright 2017 Ettus Research, a National Instruments Company +# Copyright 2017-2018 Ettus Research, a National Instruments Company # -# SPDX-License-Identifier: GPL-3.0 +# SPDX-License-Identifier: GPL-3.0-or-later # ######################################################################## @@ -12,6 +12,7 @@ SET(USRP_MPM_DBMGR_FILES ${CMAKE_CURRENT_SOURCE_DIR}/__init__.py ${CMAKE_CURRENT_SOURCE_DIR}/base.py ${CMAKE_CURRENT_SOURCE_DIR}/eiscat.py + ${CMAKE_CURRENT_SOURCE_DIR}/neon.py ${CMAKE_CURRENT_SOURCE_DIR}/lmk_eiscat.py ${CMAKE_CURRENT_SOURCE_DIR}/lmk_mg.py ${CMAKE_CURRENT_SOURCE_DIR}/magnesium.py diff --git a/mpm/python/usrp_mpm/dboard_manager/__init__.py b/mpm/python/usrp_mpm/dboard_manager/__init__.py index 3f7b6e9f0..8f6e5da96 100644 --- a/mpm/python/usrp_mpm/dboard_manager/__init__.py +++ b/mpm/python/usrp_mpm/dboard_manager/__init__.py @@ -1,5 +1,5 @@ # -# Copyright 2017 Ettus Research, a National Instruments Company +# Copyright 2017-2018 Ettus Research, a National Instruments Company # # SPDX-License-Identifier: GPL-3.0-or-later # @@ -8,7 +8,7 @@ dboards module __init__.py """ from .base import DboardManagerBase from .magnesium import Magnesium +from .neon import Neon from .eiscat import EISCAT from .test import test from .unknown import unknown - diff --git a/mpm/python/usrp_mpm/dboard_manager/neon.py b/mpm/python/usrp_mpm/dboard_manager/neon.py new file mode 100644 index 000000000..e3797c7d3 --- /dev/null +++ b/mpm/python/usrp_mpm/dboard_manager/neon.py @@ -0,0 +1,281 @@ +# +# Copyright 2018 Ettus Research, a National Instruments Company +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +""" +E320 dboard (RF and control) implementation module +""" + +import threading +import time +from six import iterkeys, iteritems +from usrp_mpm import lib # Pulls in everything from C++-land +from usrp_mpm.bfrfs import BufferFS +from usrp_mpm.chips import ADF400x +from usrp_mpm.dboard_manager import DboardManagerBase +from usrp_mpm.mpmlog import get_logger +from usrp_mpm.sys_utils.udev import get_eeprom_paths +from usrp_mpm.sys_utils.uio import UIO + + +############################################################################### +# Main dboard control class +############################################################################### +class Neon(DboardManagerBase): + """ + Holds all dboard specific information and methods of the neon dboard + """ + ######################################################################### + # Overridables + # + # See DboardManagerBase for documentation on these fields + ######################################################################### + pids = [0xe320] + rx_sensor_callback_map = { + 'ad9361_temperature': 'get_catalina_temp_sensor', + } + tx_sensor_callback_map = { + 'ad9361_temperature': 'get_catalina_temp_sensor', + } + # Maps the chipselects to the corresponding devices: + spi_chipselect = {"catalina": 0, + "adf4002": 1} + ### End of overridables ################################################# + # This map describes how the user data is stored in EEPROM. If a dboard rev + # changes the way the EEPROM is used, we add a new entry. If a dboard rev + # is not found in the map, then we go backward until we find a suitable rev + user_eeprom = { + 0: { + 'label': "e0004000.i2c", + 'offset': 1024, + 'max_size': 32786 - 1024, + 'alignment': 1024, + }, + } + + default_master_clock_rate = 16e6 + MIN_MASTER_CLK_RATE = 220e3 + MAX_MASTER_CLK_RATE = 61.44e6 + + def __init__(self, slot_idx, **kwargs): + super(Neon, self).__init__(slot_idx, **kwargs) + self.log = get_logger("Neon-{}".format(slot_idx)) + self.log.trace("Initializing Neon daughterboard, slot index %d", + self.slot_idx) + self.rev = int(self.device_info['rev']) + self.log.trace("This is a rev: {}".format(chr(65 + self.rev))) + # These will get updated during init() + self.master_clock_rate = None + # Predeclare some attributes to make linter happy: + self.catalina = None + self.eeprom_fs = None + self.eeprom_path = None + # Now initialize all peripherals. If that doesn't work, put this class + # into a non-functional state (but don't crash, or we can't talk to it + # any more): + try: + self._init_periphs() + self._periphs_initialized = True + except Exception as ex: + self.log.error("Failed to initialize peripherals: %s", + str(ex)) + self._periphs_initialized = False + + def _init_periphs(self): + """ + Initialize power and peripherals that don't need user-settings + """ + self.log.debug("Loading C++ drivers...") + # Setup the ADF4002 + adf4002_spi = lib.spi.make_spidev( + str(self._spi_nodes['adf4002']), + 1000000, # Speed (Hz) + 0 # SPI mode + ) + self.log.trace("Initializing ADF4002.") + from usrp_mpm.periph_manager.e320 import E320_DEFAULT_INT_CLOCK_FREQ + self.adf4002 = ADF400x(adf4002_spi, + freq=E320_DEFAULT_INT_CLOCK_FREQ, + parent_log=self.log) + # Setup Catalina / the Neon Manager + self._device = lib.dboards.neon_manager( + self._spi_nodes['catalina'] + ) + self.catalina = self._device.get_radio_ctrl() + self.log.trace("Loaded C++ drivers.") + self._init_cat_api(self.catalina) + self.eeprom_fs, self.eeprom_path = self._init_user_eeprom( + self._get_user_eeprom_info(self.rev) + ) + + def _init_cat_api(self, cat): + """ + Propagate the C++ Catalina API into Python land. + """ + def export_method(obj, method): + " Export a method object, including docstring " + meth_obj = getattr(obj, method) + def func(*args): + " Functor for storing docstring too " + return meth_obj(*args) + func.__doc__ = meth_obj.__doc__ + return func + self.log.trace("Forwarding AD9361 methods to Neon class...") + for method in [ + x for x in dir(self.catalina) + if not x.startswith("_") and \ + callable(getattr(self.catalina, x))]: + self.log.trace("adding {}".format(method)) + setattr(self, method, export_method(cat, method)) + + def _get_user_eeprom_info(self, rev): + """ + Return an EEPROM access map (from self.user_eeprom) based on the rev. + """ + rev_for_lookup = rev + while rev_for_lookup not in self.user_eeprom: + if rev_for_lookup < 0: + raise RuntimeError("Could not find a user EEPROM map for " + "revision %d!", rev) + rev_for_lookup -= 1 + assert rev_for_lookup in self.user_eeprom, \ + "Invalid EEPROM lookup rev!" + return self.user_eeprom[rev_for_lookup] + + def _init_user_eeprom(self, eeprom_info): + """ + Reads out user-data EEPROM, and intializes a BufferFS object from that. + """ + self.log.trace("Initializing EEPROM user data...") + eeprom_paths = get_eeprom_paths(eeprom_info.get('label')) + self.log.trace("Found the following EEPROM paths: `{}'".format( + eeprom_paths)) + eeprom_path = eeprom_paths[self.slot_idx] + self.log.trace("Selected EEPROM path: `{}'".format(eeprom_path)) + user_eeprom_offset = eeprom_info.get('offset', 0) + self.log.trace("Selected EEPROM offset: %d", user_eeprom_offset) + user_eeprom_data = open(eeprom_path, 'rb').read()[user_eeprom_offset:] + self.log.trace("Total EEPROM size is: %d bytes", len(user_eeprom_data)) + return BufferFS( + user_eeprom_data, + max_size=eeprom_info.get('max_size'), + alignment=eeprom_info.get('alignment', 1024), + log=self.log + ), eeprom_path + + def init(self, args): + if not self._periphs_initialized: + error_msg = "Cannot run init(), peripherals are not initialized!" + self.log.error(error_msg) + raise RuntimeError(error_msg) + master_clock_rate = \ + float(args.get('master_clock_rate', + self.default_master_clock_rate)) + assert self.MIN_MASTER_CLK_RATE <= master_clock_rate <= self.MAX_MASTER_CLK_RATE, \ + "Invalid master clock rate: {:.02f} MHz".format( + master_clock_rate / 1e6) + master_clock_rate_changed = master_clock_rate != self.master_clock_rate + if master_clock_rate_changed: + self.master_clock_rate = master_clock_rate + self.log.debug("Updating master clock rate to {:.02f} MHz!".format( + self.master_clock_rate / 1e6 + )) + # Some default chains on -- needed for setup purposes + self.catalina.set_active_chains(True, False, True, False) + self.catalina.set_clock_rate(self.master_clock_rate) + + return True + + def get_user_eeprom_data(self): + """ + Return a dict of blobs stored in the user data section of the EEPROM. + """ + return { + blob_id: self.eeprom_fs.get_blob(blob_id) + for blob_id in iterkeys(self.eeprom_fs.entries) + } + + def set_user_eeprom_data(self, eeprom_data): + """ + Update the local EEPROM with the data from eeprom_data. + + The actual writing to EEPROM can take some time, and is thus kicked + into a background task. Don't call set_user_eeprom_data() quickly in + succession. Also, while the background task is running, reading the + EEPROM is unavailable and MPM won't be able to reboot until it's + completed. + However, get_user_eeprom_data() will immediately return the correct + data after this method returns. + """ + for blob_id, blob in iteritems(eeprom_data): + self.eeprom_fs.set_blob(blob_id, blob) + self.log.trace("Writing EEPROM info to `{}'".format(self.eeprom_path)) + eeprom_offset = self.user_eeprom[self.rev]['offset'] + def _write_to_eeprom_task(path, offset, data, log): + " Writer task: Actually write to file " + # Note: This can be sped up by only writing sectors that actually + # changed. To do so, this function would need to read out the + # current state of the file, do some kind of diff, and then seek() + # to the different sectors. When very large blobs are being + # written, it doesn't actually help all that much, of course, + # because in that case, we'd anyway be changing most of the EEPROM. + with open(path, 'r+b') as eeprom_file: + log.trace("Seeking forward to `{}'".format(offset)) + eeprom_file.seek(eeprom_offset) + log.trace("Writing a total of {} bytes.".format( + len(self.eeprom_fs.buffer))) + eeprom_file.write(data) + log.trace("EEPROM write complete.") + thread_id = "eeprom_writer_task_{}".format(self.slot_idx) + if any([x.name == thread_id for x in threading.enumerate()]): + # Should this be fatal? + self.log.warn("Another EEPROM writer thread is already active!") + writer_task = threading.Thread( + target=_write_to_eeprom_task, + args=( + self.eeprom_path, + eeprom_offset, + self.eeprom_fs.buffer, + self.log + ), + name=thread_id, + ) + writer_task.start() + # Now return and let the copy finish on its own. The thread will detach + # and MPM won't terminate this process until the thread is complete. + # This does not stop anyone from killing this process (and the thread) + # while the EEPROM write is happening, though. + + def get_master_clock_rate(self): + " Return master clock rate (== sampling rate) " + return self.master_clock_rate + + def update_ref_clock_freq(self, freq): + """Update the reference clock frequency""" + self.adf4002.set_ref_freq(freq) + + ########################################################################## + # Sensors + ########################################################################## + # TODO add sensors + def get_ad9361_lo_lock(self, which): + """ + Return LO lock status (Boolean!) of AD9361. 'which' must be + either 'tx' or 'rx' + """ + # return self.catalina. + + # uhd::sensor_value_t e300_impl::_get_fe_pll_lock(const bool is_tx) + # { + # const uint32_t st = + # _global_regs->peek32(global_regs::RB32_CORE_PLL); + # const bool locked = is_tx ? ((st & 0x1) > 0) : ((st & 0x2) > 0); + # return sensor_value_t("LO", locked, "locked", "unlocked"); + # } + #return self.mykonos.get_lo_locked(which.upper()) + #FIXME: Implement on RevB + time.sleep(5) + return True + + diff --git a/mpm/python/usrp_mpm/periph_manager/CMakeLists.txt b/mpm/python/usrp_mpm/periph_manager/CMakeLists.txt index 095916ff7..0689cdda9 100644 --- a/mpm/python/usrp_mpm/periph_manager/CMakeLists.txt +++ b/mpm/python/usrp_mpm/periph_manager/CMakeLists.txt @@ -1,7 +1,7 @@ # -# Copyright 2017 Ettus Research, National Instruments Company +# Copyright 2017-2018 Ettus Research, a National Instruments Company # -# SPDX-License-Identifier: GPL-3.0 +# SPDX-License-Identifier: GPL-3.0-or-later # ######################################################################## @@ -13,6 +13,8 @@ SET(USRP_MPM_PERIPHMGR_FILES ${CMAKE_CURRENT_SOURCE_DIR}/base.py ${CMAKE_CURRENT_SOURCE_DIR}/n3xx.py ${CMAKE_CURRENT_SOURCE_DIR}/n3xx_periphs.py -) + ${CMAKE_CURRENT_SOURCE_DIR}/e320.py + ${CMAKE_CURRENT_SOURCE_DIR}/e320_periphs.py + ) LIST(APPEND USRP_MPM_FILES ${USRP_MPM_PERIPHMGR_FILES}) SET(USRP_MPM_FILES ${USRP_MPM_FILES} PARENT_SCOPE) diff --git a/mpm/python/usrp_mpm/periph_manager/e320.py b/mpm/python/usrp_mpm/periph_manager/e320.py new file mode 100644 index 000000000..ac50909ff --- /dev/null +++ b/mpm/python/usrp_mpm/periph_manager/e320.py @@ -0,0 +1,671 @@ +# +# Copyright 2018 Ettus Research, a National Instruments Company +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +""" +E320 implementation module +""" + +from __future__ import print_function +import bisect +import copy +import threading +from six import iteritems, itervalues +from usrp_mpm.components import ZynqComponents +from usrp_mpm.dboard_manager import Neon +from usrp_mpm.mpmtypes import SID +from usrp_mpm.mpmutils import assert_compat_number, str2bool +from usrp_mpm.periph_manager import PeriphManagerBase +from usrp_mpm.rpc_server import no_rpc +from usrp_mpm.sys_utils import dtoverlay +from usrp_mpm.sys_utils.udev import get_spidev_nodes +from usrp_mpm.xports import XportMgrUDP, XportMgrLiberio +from usrp_mpm.periph_manager.e320_periphs import MboardRegsControl + +E320_DEFAULT_INT_CLOCK_FREQ = 20e6 +E320_DEFAULT_EXT_CLOCK_FREQ = 10e6 +E320_DEFAULT_CLOCK_SOURCE = 'internal' +E320_DEFAULT_TIME_SOURCE = 'internal' +E320_DEFAULT_ENABLE_GPS = True +E320_DEFAULT_FPGPIO_VOLTAGE = 0 +E320_FPGA_COMPAT = (3, 0) +E320_MONITOR_THREAD_INTERVAL = 1.0 # seconds # TODO Verify this +E320_DBOARD_SLOT_IDX = 0 + + +############################################################################### +# Transport managers +############################################################################### +class E320XportMgrUDP(XportMgrUDP): + "E320-specific UDP configuration" + xbar_dev = "/dev/crossbar0" + iface_config = { + 'sfp0': { + 'label': 'misc-enet-regs', + 'xbar': 0, + 'xbar_port': 0, + 'ctrl_src_addr': 0, + } + } + +class E320XportMgrLiberio(XportMgrLiberio): + " E320-specific Liberio configuration " + max_chan = 6 + xbar_dev = "/dev/crossbar0" + xbar_port = 1 + +############################################################################### +# Main Class +############################################################################### +class e320(ZynqComponents, PeriphManagerBase): + """ + Holds E320 specific attributes and methods + """ + ######################################################################### + # Overridables + # + # See PeriphManagerBase for documentation on these fields + ######################################################################### + description = "E300-Series Device" + pids = {0xe320: 'e320'} + mboard_eeprom_addr = "e0004000.i2c" + mboard_eeprom_offset = 0 + mboard_eeprom_max_len = 256 + mboard_info = {"type": "e3xx", + "product": "e320" + } + mboard_max_rev = 2 # RevB + mboard_sensor_callback_map = { + # FIXME add sensors + } + max_num_dboards = 1 + crossbar_base_port = 2 # It's 2 because 0,1 are SFP,DMA + + # We're on a Zynq target, so the following two come from the Zynq standard + # device tree overlay (tree/arch/arm/boot/dts/zynq-7000.dtsi) + dboard_spimaster_addrs = ["e0006000.spi", "e0007000.spi"] + # E320-specific settings + # Label for the mboard UIO + mboard_regs_label = "mboard-regs" + # Override the list of updateable components + updateable_components = { + 'fpga': { + 'callback': "update_fpga", + 'path': '/lib/firmware/{}.bin', + 'reset': True, + }, + 'dts': { + 'callback': "update_dts", + 'path': '/lib/firmware/{}.dts', + 'output': '/lib/firmware/{}.dtbo', + 'reset': False, + }, + } + + @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']] + + ########################################################################### + # Ctor and device initialization tasks + ########################################################################### + def __init__(self, args): + super(e320, self).__init__(args) + if not self._device_initialized: + # Don't try and figure out what's going on. Just give up. + return + self._tear_down = False + self._status_monitor_thread = None + self._ext_clock_freq = E320_DEFAULT_EXT_CLOCK_FREQ + self._clock_source = None + self._time_source = None + self._available_endpoints = list(range(256)) + self.dboard = self.dboards[E320_DBOARD_SLOT_IDX] + try: + self._init_peripherals(args) + except Exception as ex: + self.log.error("Failed to initialize motherboard: %s", str(ex)) + self._initialization_status = str(ex) + self._device_initialized = False + + def _init_dboards(self, _, override_dboard_pids, default_args): + """ + Initialize all the daughterboards + + (dboard_infos) -- N/A + override_dboard_pids -- List of dboard PIDs to force + default_args -- Default args + """ + # Override the base class's implementation in order to avoid initializing our one "dboard" + # in the same way that, for example, N310's dboards are initialized. Specifically, + # - skip dboard EEPROM setup (we don't have one) + # - change the way we handle SPI devices + if override_dboard_pids: + self.log.warning("Overriding daughterboard PIDs with: {}" + .format(override_dboard_pids)) + raise NotImplementedError("Can't override dboard pids") + # The DBoard PID is the same as the MBoard PID + db_pid = list(self.pids.keys())[0] + # Set up the SPI nodes + spi_nodes = [] + for spi_addr in self.dboard_spimaster_addrs: + for spi_node in get_spidev_nodes(spi_addr): + bisect.insort(spi_nodes, spi_node) + self.log.trace("Found spidev nodes: {0}".format(spi_nodes)) + + if not spi_nodes: + self.log.warning("No SPI nodes for dboard %d.", E320_DBOARD_SLOT_IDX) + dboard_info = { + 'eeprom_md': self.mboard_info, + 'eeprom_rawdata': self._eeprom_rawdata, + 'pid': db_pid, + 'spi_nodes': spi_nodes, + 'default_args': default_args, + } + # This will actually instantiate the dboard class: + self.dboards.append(Neon(E320_DBOARD_SLOT_IDX, **dboard_info)) + self.log.info("Found %d daughterboard(s).", len(self.dboards)) + + 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( + E320_FPGA_COMPAT, + self.mboard_regs_control.get_compat_number(), + component="FPGA", + fail_on_old_minor=True, + log=self.log + ) + + def _init_ref_clock_and_time(self, default_args): + """ + Initialize clock and time sources. After this function returns, the + reference signals going to the FPGA are valid. + """ + self._ext_clock_freq = float( + default_args.get('ext_clock_freq', E320_DEFAULT_EXT_CLOCK_FREQ) + ) + if not self.dboards: + self.log.warning( + "No dboards found, skipping setting clock and time source " + "configuration." + ) + self._clock_source = E320_DEFAULT_CLOCK_SOURCE + self._time_source = E320_DEFAULT_TIME_SOURCE + else: + self.set_clock_source( + default_args.get('clock_source', E320_DEFAULT_CLOCK_SOURCE) + ) + self.set_time_source( + default_args.get('time_source', E320_DEFAULT_TIME_SOURCE) + ) + + def _monitor_status(self): + """ + Status monitoring thread: This should be executed in a thread. It will + continuously monitor status of the following peripherals: + + - GPS lock + """ + self.log.trace("Launching monitor loop...") + cond = threading.Condition() + cond.acquire() + while not self._tear_down: + gps_locked = self.get_gps_lock_sensor()['value'] == 'true' + # Now wait + if cond.wait_for( + lambda: self._tear_down, + E320_MONITOR_THREAD_INTERVAL): + break + cond.release() + self.log.trace("Terminating monitor loop.") + + def _init_peripherals(self, args): + """ + Turn on all peripherals. This may throw an error on failure, so make + sure to catch it. + + Peripherals are initialized in the order of least likely to fail, to most + likely. + """ + # Sanity checks + assert self.mboard_info.get('product') in self.pids.values(), \ + "Device product could not be determined!" + # Init Mboard Regs + self.mboard_regs_control = MboardRegsControl( + self.mboard_regs_label, self.log) + self.mboard_regs_control.get_git_hash() + self.mboard_regs_control.get_build_timestamp() + self._check_fpga_compat() + self._update_fpga_type() + # Init peripherals + self.enable_gps( + enable=str2bool( + args.get('enable_gps', E320_DEFAULT_ENABLE_GPS) + ) + ) + self.enable_fp_gpio( + voltage=args.get( + 'fp_gpio_voltage', + E320_DEFAULT_FPGPIO_VOLTAGE + ) + ) + # Init clocking + self._init_ref_clock_and_time(args) + # Init CHDR transports + self._xport_mgrs = { + 'udp': E320XportMgrUDP(self.log.getChild('UDP')), + 'liberio': E320XportMgrLiberio(self.log.getChild('liberio')), + } + # Spawn status monitoring thread + self.log.trace("Spawning status monitor thread...") + self._status_monitor_thread = threading.Thread( + target=self._monitor_status, + name="E320StatusMonitorThread", + daemon=True, + ) + self._status_monitor_thread.start() + # Init complete. + self.log.debug("mboard info: {}".format(self.mboard_info)) + + ########################################################################### + # 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 + if args.get("clock_source", "") != "": + self.set_clock_source(args.get("clock_source")) + if args.get("time_source", "") != "": + self.set_time_source(args.get("time_source")) + result = super(e320, self).init(args) + for xport_mgr in itervalues(self._xport_mgrs): + 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 + super(e320, self).deinit() + for xport_mgr in itervalues(self._xport_mgrs): + xport_mgr.deinit() + self.log.trace("Resetting SID pool...") + self._available_endpoints = list(range(256)) + + def tear_down(self): + """ + Tear down all members that need to be specially handled before + deconstruction. + For E320, this means the overlay. + """ + self.log.trace("Tearing down E320 device...") + self._tear_down = True + if self._device_initialized: + self._status_monitor_thread.join(3 * E320_MONITOR_THREAD_INTERVAL) + if self._status_monitor_thread.is_alive(): + self.log.error("Could not terminate monitor thread! This could result in resource leaks.") + active_overlays = self.list_active_overlays() + self.log.trace("E320 has active device tree overlays: {}".format( + active_overlays + )) + for overlay in active_overlays: + dtoverlay.rm_overlay(overlay) + + ########################################################################### + # Transport API + ########################################################################### + def request_xport( + self, + dst_address, + suggested_src_address, + xport_type + ): + """ + See PeriphManagerBase.request_xport() for docs. + """ + # Try suggested address first, then just pick the first available one: + src_address = suggested_src_address + if src_address not in self._available_endpoints: + if not self._available_endpoints: + raise RuntimeError( + "Depleted pool of SID endpoints for this device!") + else: + src_address = self._available_endpoints[0] + sid = SID(src_address << 16 | dst_address) + # Note: This SID may change its source address! + self.log.trace( + "request_xport(dst=0x%04X, suggested_src_address=0x%04X, xport_type=%s): " \ + "operating on temporary SID: %s", + dst_address, suggested_src_address, str(xport_type), str(sid)) + # FIXME token! + assert self.mboard_info['rpc_connection'] in ('remote', 'local') + if self.mboard_info['rpc_connection'] == 'remote': + return self._xport_mgrs['udp'].request_xport( + sid, + xport_type, + ) + elif self.mboard_info['rpc_connection'] == 'local': + return self._xport_mgrs['liberio'].request_xport( + sid, + xport_type, + ) + + def commit_xport(self, xport_info): + """ + See PeriphManagerBase.commit_xport() for docs. + + Reminder: All connections are incoming, i.e. "send" or "TX" means + remote device to local device, and "receive" or "RX" means this local + device to remote device. "Remote device" can be, for example, a UHD + session. + """ + ## Go, go, go + assert self.mboard_info['rpc_connection'] in ('remote', 'local') + sid = SID(xport_info['send_sid']) + self._available_endpoints.remove(sid.src_ep) + self.log.debug("Committing transport for SID %s, xport info: %s", + str(sid), str(xport_info)) + if self.mboard_info['rpc_connection'] == 'remote': + return self._xport_mgrs['udp'].commit_xport(sid, xport_info) + elif self.mboard_info['rpc_connection'] == 'local': + return self._xport_mgrs['liberio'].commit_xport(sid, xport_info) + + ########################################################################### + # 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': self.updateable_components.get('fpga', {}).get('type',""), + }) + return device_info + + ########################################################################### + # Clock/Time API + ########################################################################### + def get_clock_sources(self): + " Lists all available clock sources. " + self.log.trace("Listing available clock sources...") + return ('external', 'internal', 'gpsdo') + + def get_clock_source(self): + " Returns the currently selected clock source " + return self._clock_source + + def set_clock_source(self, *args): + """ + Switch reference clock. + + Throws if clock_source is not a valid value. + """ + clock_source = args[0] + assert clock_source in self.get_clock_sources() + self.log.debug("Setting clock source to `{}'".format(clock_source)) + if clock_source == self.get_clock_source(): + self.log.trace("Nothing to do -- clock source already set.") + return + self._clock_source = clock_source + ref_clk_freq = self.get_ref_clock_freq() + self.mboard_regs_control.set_clock_source(clock_source, ref_clk_freq) + self.log.debug("Reference clock frequency is: {} MHz".format( + ref_clk_freq/1e6 + )) + self.dboard.update_ref_clock_freq(ref_clk_freq) + + 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. + """ + # Other frequencies have not been tested + assert freq in (10e6, 20e6) + self.log.debug("We've been told the external reference clock " \ + "frequency is {} MHz.".format(freq / 1e6)) + if self._ext_clock_freq == freq: + self.log.trace("New external reference clock frequency " \ + "assignment matches previous assignment. Ignoring " \ + "update command.") + return + self._ext_clock_freq = freq + if self.get_clock_source() == 'external': + for slot, dboard in enumerate(self.dboards): + if hasattr(dboard, 'update_ref_clock_freq'): + self.log.trace( + "Updating reference clock on dboard %d to %f MHz...", + slot, freq/1e6 + ) + dboard.update_ref_clock_freq(freq) + + + def get_ref_clock_freq(self): + " Returns the currently active reference clock frequency" + clock_source = self.get_clock_source() + if clock_source == "internal" or clock_source == "gpsdo": + return E320_DEFAULT_INT_CLOCK_FREQ + elif clock_source == "external": + return self._ext_clock_freq + + def get_time_sources(self): + " Returns list of valid time sources " + return ['internal', 'external', 'gpsdo'] + + def get_time_source(self): + " Return the currently selected time source " + return self._time_source + + def set_time_source(self, time_source): + " Set a time source " + assert time_source in self.get_time_sources() + if time_source == self.get_time_source(): + self.log.trace("Nothing to do -- time source already set.") + return + self._time_source = time_source + self.mboard_regs_control.set_time_source(time_source, self.get_ref_clock_freq()) + + ########################################################################### + # Hardware peripheral controls + ########################################################################### + + def set_fp_gpio_master(self, value): + """set driver for front panel GPIO + Arguments: + value {unsigned} -- value is a single bit bit mask of 12 pins GPIO + """ + self.mboard_regs_control.set_fp_gpio_master(value) + + def get_fp_gpio_master(self): + """get "who" is driving front panel gpio + The return value is a bit mask of 8 pins GPIO. + 0: means the pin is driven by PL + 1: means the pin is driven by PS + """ + return self.mboard_regs_control.get_fp_gpio_master() + + def set_fp_gpio_radio_src(self, value): + """set driver for front panel GPIO + Arguments: + value {unsigned} -- value is 2-bit bit mask of 8 pins GPIO + 00: means the pin is driven by radio 0 + 01: means the pin is driven by radio 1 + """ + self.mboard_regs_control.set_fp_gpio_radio_src(value) + + def get_fp_gpio_radio_src(self): + """get which radio is driving front panel gpio + The return value is 2-bit bit mask of 8 pins GPIO. + 00: means the pin is driven by radio 0 + 01: means the pin is driven by radio 1 + """ + return self.mboard_regs_control.get_fp_gpio_radio_src() + + def enable_gps(self, enable): + """ + Turn power to the GPS (CLK_GPS_PWR_EN) off or on. + """ + self.mboard_regs_control.enable_gps(enable) + + def enable_fp_gpio(self, voltage): + """ + Turn power to the front panel GPIO off or on and set voltage + to (1.8, 2.5, 3.3V) and setting to 0 turns off GPIO. + """ + self.log.trace("{} power to front-panel GPIO".format( + "Enabling" if voltage == 0 else "Disabling" + )) + self.mboard_regs_control.enable_fp_gpio(voltage) + + def set_fp_gpio_voltage(self, value): + """ + Set Front Panel GPIO voltage (1.8, 2.5 or 3.3 Volts) + """ + self.log.trace("Setting front-panel GPIO voltage to {:3.1f} V".format(value)) + self.mboard_regs_control.set_fp_gpio_voltage(value) + + def get_fp_gpio_voltage(self): + """ + Get Front Panel GPIO voltage (1.8, 2.5 or 3.3 Volts) + """ + value = self.mboard_regs_control.get_fp_gpio_voltage() + self.log.trace("Current front-panel GPIO voltage {:3.1f} V".format(value)) + return value + + def set_channel_mode(self, channel_mode): + "Set channel mode in FPGA and select which tx channel to use" + self.mboard_regs_control.set_channel_mode(channel_mode) + + ########################################################################### + # Sensors + ########################################################################### + def get_temp_sensor(self): + """ + Get temperature sensor reading of the E320. + """ + # TODO: This is Catalina's temperature. Do we want to return a different temp? + return self.catalina.get_temperature() + + def get_gps_lock_sensor(self): + """ + Get lock status of GPS as a sensor dict + """ + self.log.trace("Reading status GPS lock pin from port expander") + raise NotImplementedError("GPS lock not implemented") + # FIXME put it in a register + # TODO: implement get_gps_lock, splits up functionality + #gps_locked = bool(self._gpios.get("GPS-LOCKOK")) + #return { + # 'name': 'gps_lock', + # 'type': 'BOOLEAN', + # 'unit': 'locked' if gps_locked else 'unlocked', + # 'value': str(gps_locked).lower(), + #} + + # TODO: Add other GPS sensors (time, TPV, SKY, etc.) + # TODO: Add all physical sensors we can + + ########################################################################### + # EEPROMs + ########################################################################### + def get_mb_eeprom(self): + """ + Return a dictionary with EEPROM contents. + + All key/value pairs are string -> string. + + We don't actually return the EEPROM contents, instead, we return the + mboard info again. This filters the EEPROM contents to what we think + the user wants to know/see. + """ + return self.mboard_info + + def set_mb_eeprom(self, eeprom_vals): + """ + See PeriphManagerBase.set_mb_eeprom() for docs. + """ + self.log.warn("Called set_mb_eeprom(), but not implemented!") + raise NotImplementedError + + def get_db_eeprom(self, dboard_idx): + """ + See PeriphManagerBase.get_db_eeprom() for docs. + """ + if dboard_idx != E320_DBOARD_SLOT_IDX: + self.log.warn("Trying to access invalid dboard index {}. " + "Using the only dboard.".format(dboard_idx)) + db_eeprom_data = copy.copy(self.dboard.device_info) + for blob_id, blob in iteritems(self.dboard.get_user_eeprom_data()): + if blob_id in db_eeprom_data: + self.log.warn("EEPROM user data contains invalid blob ID " + "%s", blob_id) + else: + db_eeprom_data[blob_id] = blob + return db_eeprom_data + + def set_db_eeprom(self, dboard_idx, eeprom_data): + """ + Write new EEPROM contents with eeprom_map. + + Arguments: + dboard_idx -- Slot index of dboard (can only be E320_DBOARD_SLOT_IDX) + eeprom_data -- Dictionary of EEPROM data to be written. It's up to the + specific device implementation on how to handle it. + """ + if dboard_idx != E320_DBOARD_SLOT_IDX: + self.log.warn("Trying to access invalid dboard index {}. " + "Using the only dboard.".format(dboard_idx)) + safe_db_eeprom_user_data = {} + for blob_id, blob in iteritems(eeprom_data): + if blob_id in self.dboard.device_info: + error_msg = "Trying to overwrite read-only EEPROM " \ + "entry `{}'!".format(blob_id) + self.log.error(error_msg) + raise RuntimeError(error_msg) + if not isinstance(blob, str) and not isinstance(blob, bytes): + error_msg = "Blob data for ID `{}' is not a " \ + "string!".format(blob_id) + self.log.error(error_msg) + raise RuntimeError(error_msg) + assert isinstance(blob, str) + safe_db_eeprom_user_data[blob_id] = blob.encode('ascii') + self.dboard.set_user_eeprom_data(safe_db_eeprom_user_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_type = self.mboard_regs_control.get_fpga_type() + self.log.debug("Updating mboard FPGA type info to {}".format(fpga_type)) + self.updateable_components['fpga']['type'] = fpga_type diff --git a/mpm/python/usrp_mpm/periph_manager/e320_periphs.py b/mpm/python/usrp_mpm/periph_manager/e320_periphs.py new file mode 100644 index 000000000..d98c5a0e5 --- /dev/null +++ b/mpm/python/usrp_mpm/periph_manager/e320_periphs.py @@ -0,0 +1,383 @@ +# +# Copyright 2018 Ettus Research, a National Instruments Company +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +""" +E320 peripherals +""" + +import datetime +from usrp_mpm.sys_utils.sysfs_gpio import SysFSGPIO, GPIOBank +from usrp_mpm.sys_utils.uio import UIO + +# Map register values to SFP transport types +E320_SFP_TYPES = { + 0: "", # Port not connected + 1: "1G", + 2: "10G", + 3: "A", # Aurora +} + +E320_FPGA_TYPES_BY_SFP = { + (""): "", + ("1G"): "1G", + ("10G"): "XG", + ("A"): "AA", +} + +class FrontpanelGPIO(GPIOBank): + """ + Abstraction layer for the front panel GPIO + """ + EMIO_BASE = 54 + FP_GPIO_OFFSET = 32 # Bit offset within the ps_gpio_* pins + + def __init__(self, ddr): + GPIOBank.__init__( + self, + 'zynq_gpio', + self.FP_GPIO_OFFSET + self.EMIO_BASE, + 0xFF, # use_mask + ddr + ) + +class MboardRegsControl(object): + """ + Control the FPGA Motherboard registers + """ + # Motherboard registers + MB_COMPAT_NUM = 0x0000 + MB_DATESTAMP = 0x0004 + MB_GIT_HASH = 0x0008 + MB_SCRATCH = 0x000C + MB_NUM_CE = 0x0010 + MB_NUM_IO_CE = 0x0014 + MB_CLOCK_CTRL = 0x0018 + MB_XADC_RB = 0x001C + MB_BUS_CLK_RATE = 0x0020 + MB_BUS_COUNTER = 0x0024 + MB_SFP_PORT_INFO = 0x0028 + MB_GPIO_CTRL = 0x002C + MB_GPIO_MASTER = 0x0030 + MB_GPIO_RADIO_SRC = 0x0034 + MB_GPS_CTRL = 0x0038 + MB_GPS_STATUS = 0x003C + MB_DBOARD_CTRL = 0x0040 + MB_DBOARD_STATUS = 0x0044 + + # Bitfield locations for the MB_CLOCK_CTRL register. + MB_CLOCK_CTRL_PPS_SEL_INT = 0 + MB_CLOCK_CTRL_PPS_SEL_EXT = 1 + MB_CLOCK_CTRL_REF_SEL = 2 + MB_CLOCK_CTRL_REF_CLK_LOCKED = 3 + + # Bitfield locations for the MB_GPIO_CTRL register. + MB_GPIO_CTRL_BUFFER_OE_N = 0 + MB_GPIO_CTRL_EN_VAR_SUPPLY = 1 + MB_GPIO_CTRL_EN_2V5 = 2 + MB_GPIO_CTRL_EN_3V3 = 3 + + # Bitfield locations for the MB_GPS_CTRL register. + MB_GPS_CTRL_PWR_EN = 0 + MB_GPS_CTRL_RST_N = 1 + MB_GPS_CTRL_INITSURV_N = 2 + + # Bitfield locations for the MB_GPS_STATUS register. + MB_GPS_STATUS_LOCK = 0 + MB_GPS_STATUS_ALARM = 1 + MB_GPS_STATUS_PHASELOCK = 2 + MB_GPS_STATUS_SURVEY = 3 + MB_GPS_STATUS_WARMUP = 4 + + # Bitfield locations for the MB_DBOARD_CTRL register. + MB_DBOARD_CTRL_MIMO = 0 + MB_DBOARD_CTRL_TX_CHAN_SEL = 1 + + def __init__(self, label, log): + self.log = log + self.regs = UIO( + label=label, + read_only=False + ) + self.poke32 = self.regs.poke32 + self.peek32 = self.regs.peek32 + + 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 ) + """ + with self.regs.open(): + compat_number = self.peek32(self.MB_COMPAT_NUM) + minor = compat_number & 0xff + major = (compat_number>>16) & 0xff + return (major, minor) + + def enable_fp_gpio(self, value): + """ Enable front panel GPIO buffers and power supply + and set voltage 1.8, 2.5 or 3.3 V + Setting value to 0 would disable gpio + """ + if value == 0: + enable = False + else: + enable = True + self.set_fp_gpio_voltage(value) + mask = 0xFFFFFFFF ^ ((0b1 << self.MB_GPIO_CTRL_BUFFER_OE_N) | \ + (0b1 << self.MB_GPIO_CTRL_EN_VAR_SUPPLY)) + with self.regs.open(): + reg_val = self.peek32(self.MB_GPIO_CTRL) & mask + reg_val = reg_val | (not enable << self.MB_GPIO_CTRL_BUFFER_OE_N) | \ + (enable << self.MB_GPIO_CTRL_EN_VAR_SUPPLY) + self.log.trace("Writing MB_GPIO_CTRL to 0x{:08X}".format(reg_val)) + return self.poke32(self.MB_GPIO_CTRL, reg_val) + + def set_fp_gpio_voltage(self, value): + """ Set Front Panel GPIO voltage (in volts) + 3V3 2V5 | Voltage + ----------------- + 0 0 | 1.8 V + 0 1 | 2.5 V + 1 0 | 3.3 V + Arguments: + value : 1.8, 2.5 or 3.3 + """ + assert value in (1.8, 2.5, 3.3) + if value == 1.8: + voltage_reg = 0 + elif value == 2.5: + voltage_reg = 1 + elif value == 3.3: + voltage_reg = 2 + mask = 0xFFFFFFFF ^ ((0b1 << self.MB_GPIO_CTRL_EN_3V3) | \ + (0b1 << self.MB_GPIO_CTRL_EN_2V5)) + with self.regs.open(): + reg_val = self.peek32(self.MB_GPIO_CTRL) & mask + reg_val = reg_val | (voltage_reg << self.MB_GPIO_CTRL_EN_2V5) + self.log.trace("Writing MB_GPIO_CTRL to 0x{:08X}".format(reg_val)) + return self.poke32(self.MB_GPIO_CTRL, reg_val) + + def get_fp_gpio_voltage(self): + """ + Get Front Panel GPIO voltage (in volts) + """ + mask = 0x3 << self.MB_GPIO_CTRL_EN_2V5 + voltage = [1.8, 2.5, 3.3] + with self.regs.open(): + reg_val = (self.peek32(self.MB_GPIO_CTRL) & mask) >> self.MB_GPIO_CTRL_EN_2V5 + return voltage[reg_val] + + def set_fp_gpio_master(self, value): + """set driver for front panel GPIO + Arguments: + value {unsigned} -- value is a single bit bit mask of 8 pins GPIO + """ + with self.regs.open(): + return self.poke32(self.MB_GPIO_MASTER, value) + + def get_fp_gpio_master(self): + """get "who" is driving front panel gpio + The return value is a bit mask of 8 pins GPIO. + 0: means the pin is driven by PL + 1: means the pin is driven by PS + """ + with self.regs.open(): + return self.peek32(self.MB_GPIO_MASTER) & 0xfff + + def set_fp_gpio_radio_src(self, value): + """set driver for front panel GPIO + Arguments: + value {unsigned} -- value is 2-bit bit mask of 8 pins GPIO + 00: means the pin is driven by radio 0 + 01: means the pin is driven by radio 1 + """ + with self.regs.open(): + return self.poke32(self.MB_GPIO_RADIO_SRC, value) + + def get_fp_gpio_radio_src(self): + """get which radio is driving front panel gpio + The return value is 2-bit bit mask of 8 pins GPIO. + 00: means the pin is driven by radio 0 + 01: means the pin is driven by radio 1 + """ + with self.regs.open(): + return self.peek32(self.MB_GPIO_RADIO_SRC) & 0xffffff + + def get_build_timestamp(self): + """ + Returns the build date/time for the FPGA image. + The return is datetime string with the ISO 8601 format + (YYYY-MM-DD HH:MM:SS.mmmmmm) + """ + with self.regs.open(): + datestamp_rb = self.peek32(self.MB_DATESTAMP) + if datestamp_rb > 0: + dt_str = datetime.datetime( + year=((datestamp_rb>>17)&0x3F)+2000, + month=(datestamp_rb>>23)&0x0F, + day=(datestamp_rb>>27)&0x1F, + hour=(datestamp_rb>>12)&0x1F, + minute=(datestamp_rb>>6)&0x3F, + second=((datestamp_rb>>0)&0x3F)) + self.log.trace("FPGA build timestamp: {}".format(str(dt_str))) + return str(dt_str) + else: + # Compatibility with FPGAs without datestamp capability + return '' + + def get_git_hash(self): + """ + Returns the GIT hash for the FPGA build. + The return is a tuple of + 2 numbers: (short git hash, bool: is the tree dirty?) + """ + with self.regs.open(): + git_hash_rb = self.peek32(self.MB_GIT_HASH) + git_hash = git_hash_rb & 0x0FFFFFFF + tree_dirty = ((git_hash_rb & 0xF0000000) > 0) + dirtiness_qualifier = 'dirty' if tree_dirty else 'clean' + self.log.trace("FPGA build GIT Hash: {:07x} ({})".format( + git_hash, dirtiness_qualifier)) + return (git_hash, dirtiness_qualifier) + + def set_time_source(self, time_source, ref_clk_freq): + """ + Set time source + """ + pps_sel_val = 0x0 + if time_source == 'internal' or time_source == 'gpsdo': + self.log.trace("Setting time source to internal (GPSDO)" + "({:.1f} MHz reference)...".format(ref_clk_freq)) + pps_sel_val = 0b1 << self.MB_CLOCK_CTRL_PPS_SEL_INT + elif time_source == 'external': + self.log.debug("Setting time source to external...") + pps_sel_val = 0b1 << self.MB_CLOCK_CTRL_PPS_SEL_EXT + else: + assert False, "Cannot set to invalid time source: {}".format(time_source) + with self.regs.open(): + reg_val = self.peek32(self.MB_CLOCK_CTRL) & 0xFFFFFF90 + # prevent glitches by writing a cleared value first, then the final value. + self.poke32(self.MB_CLOCK_CTRL, reg_val) + reg_val = reg_val | (pps_sel_val & 0x6F) + self.log.trace("Writing MB_CLOCK_CTRL to 0x{:08X}".format(reg_val)) + self.poke32(self.MB_CLOCK_CTRL, reg_val) + + def set_clock_source(self, clock_source, ref_clk_freq): + """ + Set clock source + """ + if clock_source == 'internal' or clock_source == 'gpsdo': + self.log.trace("Setting clock source to internal (GPSDO)" + "({:.1f} MHz reference)...".format(ref_clk_freq)) + ref_sel_val = 0b0 + elif clock_source == 'external': + self.log.debug("Setting clock source to external..." + "({:.1f} MHz reference)...".format(ref_clk_freq)) + ref_sel_val = 0b1 + else: + assert False, "Cannot set to invalid clock source: {}".format(clock_source) + mask = 0xFFFFFFFF ^ (0b1 << self.MB_CLOCK_CTRL_REF_SEL) + with self.regs.open(): + reg_val = self.peek32(self.MB_CLOCK_CTRL) & mask + reg_val = reg_val | (ref_sel_val << self.MB_CLOCK_CTRL_REF_SEL) + self.log.trace("Writing MB_CLOCK_CTRL to 0x{:08X}".format(reg_val)) + self.poke32(self.MB_CLOCK_CTRL, reg_val) + + def get_fpga_type(self): + """ + Reads the type of the FPGA image currently loaded + Returns a string with the type (ie 1G, XG, AU, etc.) + """ + with self.regs.open(): + sfp_info_rb = self.peek32(self.MB_SFP_PORT_INFO) + # Print the registers values as 32-bit hex values + self.log.trace("SFP Info: 0x{0:0{1}X}".format(sfp_info_rb, 8)) + try: + sfp_type = E320_SFP_TYPES.get((sfp_info_rb & 0x0000FF00) >> 8, "") + self.log.trace("SFP type: {}".format(sfp_type)) + return sfp_type + except KeyError: + self.log.warning("Unrecognized SFP type: {}" + .format(sfp_type)) + return "" + + def get_gps_locked_val(self): + """ + Get GPS LOCK status + """ + mask = 0b1 << self.MB_GPS_STATUS_LOCK + with self.regs.open(): + reg_val = self.peek32(self.MB_GPS_STATUS) & mask + gps_locked = reg_val & 0x1 #FIXME + if gps_locked: + self.log.trace("GPS locked!") + # Can return this value because the gps_locked value is on the LSB + return gps_locked + + def get_gps_status(self): + """ + Get GPS status + """ + mask = 0x1F + with self.regs.open(): + gps_status = self.peek32(self.MB_GPS_STATUS) & mask + return gps_status + + def enable_gps(self, enable): + """ + Turn power to the GPS (CLK_GPS_PWR_EN) off or on. + Power signal is GPS_3V3. + """ + self.log.trace("{} power to GPS".format( + "Enabling" if enable else "Disabling" + )) + mask = 0xFFFFFFFF ^ (0b1 << self.MB_GPS_CTRL_PWR_EN) + with self.regs.open(): + reg_val = self.peek32(self.MB_GPS_CTRL) & mask + reg_val = reg_val | (enable << self.MB_GPS_CTRL_PWR_EN) + self.log.trace("Writing MB_GPS_CTRL to 0x{:08X}".format(reg_val)) + return self.poke32(self.MB_GPS_CTRL, reg_val) + + def get_refclk_lock(self): + """ + Check the status of the reference clock (adf4002) in FPGA. + """ + mask = 0b1 << self.MB_CLOCK_CTRL_REF_CLK_LOCKED + with self.regs.open(): + reg_val = self.peek32(self.MB_CLOCK_CTRL) + locked = (reg_val & mask) > 0 + if not locked: + self.log.warning("Reference Clock reporting unlocked. " + "MB_CLOCK_CTRL reg: 0x{:08X}".format(reg_val)) + else: + self.log.trace("Reference Clock locked!") + return locked + + def set_channel_mode(self, channel_mode): + """ + Set channel mode in FPGA and select which tx channel to use + channel mode = "MIMO" for mimo + channel mode = "SISO_TX1", "SISO_TX0" for siso tx1, tx0 respectively. + """ + with self.regs.open(): + reg_val = self.peek32(self.MB_DBOARD_CTRL) + if channel_mode == "MIMO": + reg_val = (0b1 << self.MB_DBOARD_CTRL_MIMO) + self.log.trace("Setting channel mode in AD9361 interface: {}".format("2R2T" if channel_mode == 2 else "1R1T")) + else: + # Warn if user tries to set either tx0/tx1 in mimo mode + # as both will be set automatically + if channel_mode == "SISO_TX1": + # in SISO mode, Channel 1 + reg_val = (0b1 << self.MB_DBOARD_CTRL_TX_CHAN_SEL) | (0b0 << self.MB_DBOARD_CTRL_MIMO) + self.log.trace("Setting TX channel in AD9361 interface to: TX1") + elif channel_mode == "SISO_TX0": + # in SISO mode, Channel 0 + reg_val = (0b0 << self.MB_DBOARD_CTRL_TX_CHAN_SEL) | (0b0 << self.MB_DBOARD_CTRL_MIMO) + self.log.trace("Setting TX channel in AD9361 interface to: TX0") + self.log.trace("Writing MB_DBOARD_CTRL to 0x{:08X}".format(reg_val)) + self.poke32(self.MB_DBOARD_CTRL, reg_val) + |