aboutsummaryrefslogtreecommitdiffstats
path: root/mpm/python/usrp_mpm
diff options
context:
space:
mode:
authorBrent Stapleton <brent.stapleton@ettus.com>2018-04-17 19:33:23 -0700
committerBrent Stapleton <bstapleton@g.hmc.edu>2018-07-18 15:37:27 -0700
commit300a5e3f6e5e845b4b8d093222e1c51ca4640f79 (patch)
tree62f8bb7bc7d847b8f32f1fe5b4c9c06ef608080e /mpm/python/usrp_mpm
parenta1d6530ce50ca9590739ffa40464747d3db968eb (diff)
downloaduhd-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.txt5
-rw-r--r--mpm/python/usrp_mpm/dboard_manager/__init__.py4
-rw-r--r--mpm/python/usrp_mpm/dboard_manager/neon.py281
-rw-r--r--mpm/python/usrp_mpm/periph_manager/CMakeLists.txt8
-rw-r--r--mpm/python/usrp_mpm/periph_manager/e320.py671
-rw-r--r--mpm/python/usrp_mpm/periph_manager/e320_periphs.py383
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)
+