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