aboutsummaryrefslogtreecommitdiffstats
path: root/mpm/python
diff options
context:
space:
mode:
Diffstat (limited to 'mpm/python')
-rw-r--r--mpm/python/CMakeLists.txt7
-rw-r--r--mpm/python/pyusrp_periphs/x4xx/pyusrp_periphs.cpp27
-rw-r--r--mpm/python/tests/components_tests.py114
-rwxr-xr-xmpm/python/tests/run_unit_tests.py8
-rwxr-xr-xmpm/python/tests/sys_utils_tests.py5
-rwxr-xr-xmpm/python/tests/test_utilities.py28
-rw-r--r--mpm/python/usrp_mpm/bist.py64
-rw-r--r--mpm/python/usrp_mpm/chips/CMakeLists.txt6
-rwxr-xr-xmpm/python/usrp_mpm/chips/ic_reg_maps/CMakeLists.txt8
-rw-r--r--mpm/python/usrp_mpm/chips/lmx2572.py338
-rw-r--r--mpm/python/usrp_mpm/chips/max10_cpld_flash_ctrl.py235
-rw-r--r--mpm/python/usrp_mpm/components.py139
-rw-r--r--mpm/python/usrp_mpm/dboard_manager/CMakeLists.txt5
-rw-r--r--mpm/python/usrp_mpm/dboard_manager/__init__.py5
-rw-r--r--mpm/python/usrp_mpm/dboard_manager/base.py37
-rw-r--r--[-rwxr-xr-x]mpm/python/usrp_mpm/dboard_manager/dboard_iface.py68
-rw-r--r--mpm/python/usrp_mpm/dboard_manager/empty_slot.py37
-rw-r--r--mpm/python/usrp_mpm/dboard_manager/x4xx_db_iface.py144
-rw-r--r--mpm/python/usrp_mpm/dboard_manager/x4xx_debug_db.py152
-rw-r--r--mpm/python/usrp_mpm/dboard_manager/x4xx_if_test_cca.py167
-rw-r--r--mpm/python/usrp_mpm/dboard_manager/zbx.py461
-rw-r--r--mpm/python/usrp_mpm/dboard_manager/zbx_update_cpld.py218
-rw-r--r--mpm/python/usrp_mpm/mpmutils.py47
-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
-rw-r--r--mpm/python/usrp_mpm/sys_utils/CMakeLists.txt1
-rw-r--r--mpm/python/usrp_mpm/sys_utils/ectool.py45
-rw-r--r--mpm/python/usrp_mpm/sys_utils/gpio.py10
-rw-r--r--mpm/python/usrp_mpm/sys_utils/sysfs_gpio.py31
-rw-r--r--mpm/python/usrp_mpm/sys_utils/uio.py6
-rw-r--r--mpm/python/x4xx_bist1048
43 files changed, 9642 insertions, 74 deletions
diff --git a/mpm/python/CMakeLists.txt b/mpm/python/CMakeLists.txt
index 1900c4004..ba4bf075b 100644
--- a/mpm/python/CMakeLists.txt
+++ b/mpm/python/CMakeLists.txt
@@ -55,6 +55,8 @@ elseif(MPM_DEVICE STREQUAL "e320")
add_library(pyusrp_periphs SHARED pyusrp_periphs/e320/pyusrp_periphs.cpp)
elseif(MPM_DEVICE STREQUAL "e31x")
add_library(pyusrp_periphs SHARED pyusrp_periphs/e31x/pyusrp_periphs.cpp)
+elseif(MPM_DEVICE STREQUAL "x4xx")
+ add_library(pyusrp_periphs SHARED pyusrp_periphs/x4xx/pyusrp_periphs.cpp)
endif(MPM_DEVICE STREQUAL "n3xx")
if(NOT ENABLE_SIM)
@@ -114,6 +116,11 @@ elseif (ENABLE_E320)
e320_bist
DESTINATION ${RUNTIME_DIR}
)
+elseif (ENABLE_X400)
+ install(PROGRAMS
+ x4xx_bist
+ DESTINATION ${RUNTIME_DIR}
+ )
endif (ENABLE_MYKONOS)
if (HAVE_MPM_TEST_PREREQS)
diff --git a/mpm/python/pyusrp_periphs/x4xx/pyusrp_periphs.cpp b/mpm/python/pyusrp_periphs/x4xx/pyusrp_periphs.cpp
new file mode 100644
index 000000000..31b1ed6c4
--- /dev/null
+++ b/mpm/python/pyusrp_periphs/x4xx/pyusrp_periphs.cpp
@@ -0,0 +1,27 @@
+//
+// Copyright 2019 Ettus Research, a National Instruments Brand
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+#include <pybind11/pybind11.h>
+namespace py = pybind11;
+#define LIBMPM_PYTHON
+
+// Allow boost::shared_ptr<T> to be a holder class of an object (PyBind11
+// supports std::shared_ptr and std::unique_ptr out of the box)
+#include <boost/shared_ptr.hpp>
+PYBIND11_DECLARE_HOLDER_TYPE(T, boost::shared_ptr<T>);
+
+#include <mpm/i2c/i2c_python.hpp>
+#include <mpm/rfdc/rfdc_ctrl.hpp>
+#include <mpm/spi/spi_python.hpp>
+#include <mpm/types/types_python.hpp>
+
+PYBIND11_MODULE(libpyusrp_periphs, m)
+{
+ export_types(m);
+ export_spi(m);
+ export_i2c(m);
+ export_rfdc(m);
+}
diff --git a/mpm/python/tests/components_tests.py b/mpm/python/tests/components_tests.py
new file mode 100644
index 000000000..120b89121
--- /dev/null
+++ b/mpm/python/tests/components_tests.py
@@ -0,0 +1,114 @@
+#
+# Copyright 2020 Ettus Research, a National Instruments Brand
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+"""
+Tests the components classes (currently ZynqComponents)
+"""
+
+from usrp_mpm.components import ZynqComponents
+from base_tests import TestBase
+
+import copy
+import os.path
+import tempfile
+import unittest
+
+class TestZynqComponents(TestBase):
+ """
+ Test functions of the ZynqComponents class
+ """
+
+ _testcase_input = '// mpm_version foo_current_version 1.10\n' \
+ '// mpm_version foo_oldest_compatible_version 1.5\n' \
+ '// mpm_version bar_current_version 1.2\n' \
+ '// mpm_version bar_oldest_compatible_version 1.0\n' \
+ '// mpm_version baz_current_version 1.2.3\n' \
+ '// mpm_version baz_oldest_compatible_version 1.0.0\n' \
+ '// mpm_version zack 2.0\n' \
+ '// mpm_version zack_oldest_compatible_version 1.0\n' \
+ '// mpm_other_tag noname_current_version 1.2.3\n' \
+ '// other comment\n'
+
+ _testcase_result = {
+ 'bar': {'current': (1, 2), 'oldest': (1,0)},
+ 'baz': {'current': (1, 2, 3), 'oldest': (1,0,0)},
+ 'foo': {'current': (1, 10), 'oldest': (1, 5)},
+ 'zack': {'current': (2, 0), 'oldest': (1,0)},
+ }
+
+ def _write_dts_file_from_test_cases(self, content):
+ """ Write content to a temporary .dts file """
+ f = tempfile.NamedTemporaryFile(mode="w+", suffix=".dts")
+ expected = {}
+ f.write(content)
+ f.flush()
+ return f
+
+ def test_parse_dts_version_info_from_file(self):
+ """ Test function ZynqComponents._parse_dts_version_info_from_file """
+ f = self._write_dts_file_from_test_cases(self._testcase_input)
+ expected = self._testcase_result
+ result = ZynqComponents._parse_dts_version_info_from_file(f.name)
+ self.assertEqual(result, expected)
+
+ def test_verify_compatibility(self):
+ """ Test function ZynqComponents._verify_compatibility """
+ class _log_dummy():
+ def _dummy(self, *args):
+ pass
+ trace = _dummy
+ info = _dummy
+ warning = _dummy
+ error = _dummy
+
+ f = self._write_dts_file_from_test_cases(self._testcase_input)
+ compatibility = self._testcase_result
+ for version_type in ['current', 'oldest']:
+ for case in ['normal', 'smaller_mpm_minor', 'bigger_mpm_minor',
+ 'smaller_mpm_major', 'bigger_mpm_major', 'component_missing',
+ 'additional_component']:
+ compatibility_testcase = copy.deepcopy(compatibility)
+ foo_major, foo_minor = compatibility['foo'][version_type]
+ if case == 'normal':
+ compatibility_testcase['foo'][version_type] = (foo_major, foo_minor)
+ error_expected = None
+ elif case == 'smaller_mpm_minor':
+ compatibility_testcase['foo'][version_type] = (foo_major, foo_minor-1)
+ error_expected = None
+ elif case == 'bigger_mpm_minor':
+ compatibility_testcase['foo'][version_type] = (foo_major, foo_minor+1)
+ error_expected = None
+ elif case == 'smaller_mpm_major':
+ compatibility_testcase['foo'][version_type] = (foo_major-1, foo_minor)
+ if version_type == 'oldest':
+ error_expected = None
+ else:
+ error_expected = RuntimeError()
+ elif case == 'bigger_mpm_major':
+ compatibility_testcase['foo'][version_type] = (foo_major+1, foo_minor)
+ if version_type == 'oldest':
+ error_expected = RuntimeError()
+ else:
+ error_expected = None
+ elif case == 'component_missing':
+ del compatibility_testcase['foo']
+ error_expected = None
+ elif case == 'additional_component':
+ compatibility_testcase['newcomp'] = {version_type: (2, 10)}
+ error_expected = None
+ update_dict = {
+ 'compatibility': compatibility_testcase,
+ 'check_dts_for_compatibility': True,
+ }
+ filebasename, _ = os.path.splitext(f.name)
+ try:
+ self._zynqcomponents = ZynqComponents()
+ self._zynqcomponents.log = _log_dummy()
+ self._zynqcomponents._verify_compatibility(filebasename, update_dict)
+ error = None
+ except RuntimeError as r:
+ error = r
+ self.assertEqual(error.__class__, error_expected.__class__,
+ f"Unexpected result for test case {case} (version type: {version_type})")
diff --git a/mpm/python/tests/run_unit_tests.py b/mpm/python/tests/run_unit_tests.py
index c563804ae..a26fa1d22 100755
--- a/mpm/python/tests/run_unit_tests.py
+++ b/mpm/python/tests/run_unit_tests.py
@@ -13,6 +13,7 @@ import argparse
from sys_utils_tests import TestNet
from mpm_utils_tests import TestMpmUtils
from eeprom_tests import TestEeprom
+from usrp_mpm import __simulated__
import importlib.util
if importlib.util.find_spec("xmlrunner"):
@@ -25,8 +26,15 @@ TESTS = {
TestEeprom,
},
'n3xx': set(),
+ 'x4xx': set()
}
+if not __simulated__:
+ from components_tests import TestZynqComponents
+ TESTS['x4xx'].update({
+ TestZynqComponents
+ })
+
def parse_args():
"""Parse arguments when running this as a script"""
parser_help = 'Run MPM Python unittests'
diff --git a/mpm/python/tests/sys_utils_tests.py b/mpm/python/tests/sys_utils_tests.py
index 50a10e1a1..c189257c2 100755
--- a/mpm/python/tests/sys_utils_tests.py
+++ b/mpm/python/tests/sys_utils_tests.py
@@ -62,6 +62,11 @@ class TestNet(TestBase):
"""
if self.device_name == 'n3xx':
possible_ifaces = ['eth0', 'sfp0', 'sfp1']
+ elif self.device_name == 'x4xx':
+ # x4xx devices have an internal network interface
+ # TODO: change this when sfp0 is enabled
+ # possible_ifaces = ['eth0', 'sfp0', 'int0']
+ possible_ifaces = ['eth0', 'int0']
else:
possible_ifaces = ['eth0', 'sfp0']
diff --git a/mpm/python/tests/test_utilities.py b/mpm/python/tests/test_utilities.py
index 198eda5e8..942ad956a 100755
--- a/mpm/python/tests/test_utilities.py
+++ b/mpm/python/tests/test_utilities.py
@@ -90,6 +90,7 @@ class MockRegsIface(object):
self.map = register_map
self.recent_vals = {}
self.next_vals = {}
+ self.recent_addrs = []
def peek32(self, addr):
"""
@@ -110,12 +111,32 @@ class MockRegsIface(object):
"""
self.map.set_reg(addr, value)
+ self.recent_addrs.append(addr)
+
# Store written value in a list
if addr in self.recent_vals:
self.recent_vals[addr].append(value)
else:
self.recent_vals[addr] = [value]
+ def peek16(self, addr):
+ """
+ Pass the request to the 32 bit version
+ """
+ return self.peek32(addr) & 0xFFFF
+
+ def poke16(self, addr, value):
+ """
+ Pass the request to the 32 bit version
+ """
+ self.poke32(addr, value)
+
+ def get_recent_addrs(self):
+ return self.recent_addrs
+
+ def clear_recent_addrs(self):
+ self.recent_addrs = []
+
def get_recent_vals(self, addr):
"""
Returns the past values written to a given address.
@@ -123,6 +144,13 @@ class MockRegsIface(object):
"""
return self.recent_vals.get(addr, [])
+ def clear_recent_vals(self, addr):
+ """
+ Clears the past values written to a given address.
+ Useful for validating HW interaction
+ """
+ self.recent_vals[addr] = []
+
def set_next_vals(self, addr, vals):
"""
Sets a list of the next values to be read from the
diff --git a/mpm/python/usrp_mpm/bist.py b/mpm/python/usrp_mpm/bist.py
index bd0dbb8d3..6678b132d 100644
--- a/mpm/python/usrp_mpm/bist.py
+++ b/mpm/python/usrp_mpm/bist.py
@@ -18,6 +18,7 @@ from datetime import datetime
import argparse
import subprocess
from six import iteritems
+from usrp_mpm.sys_utils import ectool
##############################################################################
# Aurora/SFP BIST code
@@ -220,6 +221,7 @@ def get_product_id_from_eeprom(valid_ids, cmd='eeprom-id'):
"""Return the mboard product ID
Returns something like n300, n310, e320...
+ the eeprom parameter is needed if there are several eeprom within the system
"""
output = subprocess.check_output(
[cmd],
@@ -268,13 +270,24 @@ def gpio_set_all(gpio_bank, value, gpio_size, ddr_mask):
##############################################################################
# Common tests
##############################################################################
-def test_ddr3_with_usrp_probe():
+def test_ddr3_with_usrp_probe(extra_args=None):
"""
Run uhd_usrp_probe and scrape the output to see if the DRAM FIFO block is
reporting a good throughput. This is a bit of a roundabout way of testing
the DDR3, but it uses existing software and also tests the RFNoC pathways.
"""
- ddr3_bist_executor = 'uhd_usrp_probe --args addr=127.0.0.1'
+ dflt_args = {'addr':'127.0.0.1', 'rfnoc_num_blocks':1}
+ extra_args = extra_args or {}
+ # merge args dicts, extra_args overrides dflt_args if keys exists in both dicts
+ args = {**dflt_args, **extra_args}
+ args_str = ",".join(
+ ['{k}={v}'.format(k=k, v=v) for k, v in iteritems(args)])
+ cmd = [
+ 'uhd_usrp_probe',
+ '--args',
+ '{e}'.format(e=args_str)
+ ]
+ ddr3_bist_executor = ' '.join(cmd)
try:
output = subprocess.check_output(
ddr3_bist_executor,
@@ -335,15 +348,19 @@ def get_ref_clock_prop(clock_source, time_source, extra_args=None):
- <sensor-name>:
- locked: Boolean lock status
"""
+ dflt_args = {'addr':'127.0.0.1'}
extra_args = extra_args or {}
+ # merge args dicts, extra_args overrides dflt_args if keys exists in both dicts
+ args = {**dflt_args, **extra_args}
result = {}
- extra_args_str = ",".join(
- ['{k}={v}'.format(k=k, v=v) for k, v in iteritems(extra_args)])
+
+ args_str = ",".join(
+ ['{k}={v}'.format(k=k, v=v) for k, v in iteritems(args)])
cmd = [
'uhd_usrp_probe',
'--args',
- 'addr=127.0.0.1,clock_source={c},time_source={t},{e}'.format(
- c=clock_source, t=time_source, e=extra_args_str),
+ 'clock_source={c},time_source={t},{e}'.format(
+ c=clock_source, t=time_source, e=args_str),
'--sensor'
]
sensor_path = '/mboards/0/sensors/ref_locked'
@@ -382,6 +399,33 @@ def get_temp_sensor_value(temp_sensor_map):
and device.attributes.get('temp') is not None
}
+
+def get_iio_temp_sensor_values():
+ """
+ Read all devices in the IIO subsystem that can report a temperature and
+ returns dictionary containing {name: temperature}, where name is the
+ temperature device name and the temperature is a value in mC.
+ """
+ import pyudev
+ context = pyudev.Context()
+ iio_devs = context.list_devices(subsystem='iio')
+
+ def is_temp_dev(dev):
+ return 'in_temp_raw' in dev.attributes.available_attributes
+
+ def get_temp(dev):
+ raw = float(dev.attributes.get('in_temp_raw').decode('ascii'))
+ offset = float(dev.attributes.get('in_temp_offset').decode('ascii'))
+ scale = float(dev.attributes.get('in_temp_scale').decode('ascii'))
+ return int(scale * (raw + offset))
+
+ def get_name(dev):
+ return dev.attributes.get('name').decode('ascii')
+
+ temp_devs = [dev for dev in iio_devs if is_temp_dev(dev)]
+ return {get_name(dev): get_temp(dev) for dev in temp_devs}
+
+
def get_fan_values():
"""
Return a dict of fan name -> fan speed key/values.
@@ -395,6 +439,14 @@ def get_fan_values():
and device.attributes.get('cur_state') is not None
}
+def get_ectool_fan_values():
+ try:
+ return ectool.get_fan_rpm()
+ except RuntimeError as ex:
+ return {
+ 'error_msg': "{}".format(str(ex))
+ }
+
def get_link_up(if_name):
"""
Return a dictionary {if_name: IFLA_OPERSTATE}
diff --git a/mpm/python/usrp_mpm/chips/CMakeLists.txt b/mpm/python/usrp_mpm/chips/CMakeLists.txt
index 5c5670b14..692e4f11d 100644
--- a/mpm/python/usrp_mpm/chips/CMakeLists.txt
+++ b/mpm/python/usrp_mpm/chips/CMakeLists.txt
@@ -7,12 +7,14 @@
set(USRP_MPM_FILES ${USRP_MPM_FILES})
set(USRP_MPM_CHIP_FILES
${CMAKE_CURRENT_SOURCE_DIR}/__init__.py
+ ${CMAKE_CURRENT_SOURCE_DIR}/adf400x.py
+ ${CMAKE_CURRENT_SOURCE_DIR}/ds125df410.py
${CMAKE_CURRENT_SOURCE_DIR}/lmk04828.py
${CMAKE_CURRENT_SOURCE_DIR}/lmk04832.py
${CMAKE_CURRENT_SOURCE_DIR}/lmk03328.py
- ${CMAKE_CURRENT_SOURCE_DIR}/adf400x.py
- ${CMAKE_CURRENT_SOURCE_DIR}/ds125df410.py
${CMAKE_CURRENT_SOURCE_DIR}/lmk05318.py
+ ${CMAKE_CURRENT_SOURCE_DIR}/lmx2572.py
+ ${CMAKE_CURRENT_SOURCE_DIR}/max10_cpld_flash_ctrl.py
)
list(APPEND USRP_MPM_FILES ${USRP_MPM_CHIP_FILES})
add_subdirectory(ic_reg_maps)
diff --git a/mpm/python/usrp_mpm/chips/ic_reg_maps/CMakeLists.txt b/mpm/python/usrp_mpm/chips/ic_reg_maps/CMakeLists.txt
index 631f30264..1a49daef6 100755
--- a/mpm/python/usrp_mpm/chips/ic_reg_maps/CMakeLists.txt
+++ b/mpm/python/usrp_mpm/chips/ic_reg_maps/CMakeLists.txt
@@ -34,6 +34,14 @@ if(ENABLE_REGMAPS)
${UHD_HOST_ROOT}/lib/ic_reg_maps/gen_lmk04816_regs.py
${CMAKE_CURRENT_BINARY_DIR}/lmk04816_regs.py
)
+ REG_MAPS_GEN_SOURCE(
+ ${UHD_HOST_ROOT}/lib/ic_reg_maps/gen_lmx2572_regs.py
+ ${CMAKE_CURRENT_BINARY_DIR}/lmx2572_regs.py
+ )
+ REG_MAPS_GEN_SOURCE(
+ ${UHD_HOST_ROOT}/lib/ic_reg_maps/gen_zbx_cpld_regs.py
+ ${CMAKE_CURRENT_BINARY_DIR}/zbx_cpld_regs.py
+ )
# add an ic_reg_maps target which can be referenced outside of this subdirectory
add_custom_target(ic_reg_maps DEPENDS ${IC_REG_MAPS})
diff --git a/mpm/python/usrp_mpm/chips/lmx2572.py b/mpm/python/usrp_mpm/chips/lmx2572.py
new file mode 100644
index 000000000..e480dd53b
--- /dev/null
+++ b/mpm/python/usrp_mpm/chips/lmx2572.py
@@ -0,0 +1,338 @@
+#
+# Copyright 2019-2020 Ettus Research, a National Instruments Brand
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+"""
+LMX2572 parent driver class
+"""
+
+import math
+from builtins import object
+from usrp_mpm.mpmlog import get_logger
+from usrp_mpm.chips.ic_reg_maps import lmx2572_regs_t
+
+NUMBER_OF_LMX2572_REGISTERS = 126
+
+class LMX2572(object):
+ """
+ Generic driver class for LMX2572 access.
+ """
+
+ READ_ONLY_REGISTERS = [107, 108, 109, 110, 111, 112, 113]
+
+ def __init__(self, regs_iface, parent_log = None):
+ self.log = parent_log
+
+ self.regs_iface = regs_iface
+ assert hasattr(self.regs_iface, 'peek16')
+ assert hasattr(self.regs_iface, 'poke16')
+ self._poke16 = regs_iface.poke16
+ self._peek16 = regs_iface.peek16
+
+ self._lmx2572_regs = lmx2572_regs_t()
+
+ self._need_recalculation = True
+ self._enabled = False
+
+ @property
+ def enabled(self):
+ return self._enabled
+
+ @enabled.setter
+ def enabled(self, enable):
+ self._set_chip_enable(bool(enable))
+ self._enabled = bool(enable)
+
+ def reset(self):
+ """
+ Performs a reset of the LMX2572 by using the software reset register.
+ """
+ self._lmx2572_regs = lmx2572_regs_t()
+ self._lmx2572_regs.reset = lmx2572_regs_t.reset_t.RESET_RESET
+ self._poke16(0, self._lmx2572_regs.get_reg(0))
+ self._lmx2572_regs.reset = lmx2572_regs_t.reset_t.RESET_NORMAL_OPERATION
+ self._set_default_values()
+ self._power_up_sequence()
+
+ def commit(self):
+ """
+ Calculates the settings when needed and writes the settings to the device
+ """
+ if self._need_recalculation:
+ self._calculate_settings()
+ self._need_recalculation = False
+ self._write_registers_reference_chain()
+ self._write_registers_frequency_tuning()
+
+ def check_pll_locked(self):
+ """
+ Returns True if the PLL is locked, False otherwise.
+ """
+ # SPI MISO is multiplexed to lock detect and register readback. Reading any
+ # register when the mux is set to the lock detect will return just the lock detect signal
+ self._lmx2572_regs.muxout_ld_sel = lmx2572_regs_t.muxout_ld_sel_t.MUXOUT_LD_SEL_LOCK_DETECT
+ self._poke16(0, self._lmx2572_regs.get_reg(0))
+
+ # If the PLL is locked we expect to read 0xFFFF from any read, else 0x0000
+ value_read = self._peek16(0)
+
+ return value_read == 0xFFFF
+
+ def set_synchronization(self, enable_synchronization):
+ """
+ Enables and disables the phase synchronization
+ """
+ vco_phase_sync = lmx2572_regs_t.vco_phase_sync_en_t.VCO_PHASE_SYNC_EN_PHASE_SYNC_MODE if \
+ enable_synchronization else \
+ lmx2572_regs_t.vco_phase_sync_en_t.VCO_PHASE_SYNC_EN_NORMAL_OPERATION
+ self._lmx2572_regs.vco_phase_sync_en = vco_phase_sync
+ self._need_recalculation = True
+
+ def get_synchronization(self):
+ """
+ Returns the enabled/disabled state of the phase synchronization
+ """
+ return self._lmx2572_regs.vco_phase_sync_en == \
+ lmx2572_regs_t.vco_phase_sync_en_t.VCO_PHASE_SYNC_EN_PHASE_SYNC_MODE
+
+ def set_output_enable_all(self, enable_output):
+ """
+ Enables or disables the output on both ports
+ """
+ self._set_output_a_enable(enable_output)
+ self._set_output_b_enable(enable_output)
+
+ def _set_chip_enable(self, chip_enable):
+ """
+ Enables or disables the LMX2572 using the powerdown register
+ All other registers are maintained during powerdown
+ """
+ powerdown = lmx2572_regs_t.powerdown_t.POWERDOWN_NORMAL_OPERATION if chip_enable else \
+ lmx2572_regs_t.powerdown_t.POWERDOWN_POWER_DOWN
+ self._lmx2572_regs.powerdown = powerdown
+ self._poke16(0, self._lmx2572_regs.get_reg(0))
+
+ def peek16(self, address):
+ """
+ Wraps _peek16 to account for mux_ld_sel
+ """
+ # SPI MISO is multiplexed to lock detect and register readback. Set the mux to register
+ # readback before trying to read the register.
+ self._lmx2572_regs.muxout_ld_sel = \
+ lmx2572_regs_t.muxout_ld_sel_t.MUXOUT_LD_SEL_REGISTER_READBACK
+ self._poke16(0, self._lmx2572_regs.get_reg(0))
+
+ value_read = self._peek16(address)
+
+ self._lmx2572_regs.muxout_ld_sel = lmx2572_regs_t.muxout_ld_sel_t.MUXOUT_LD_SEL_LOCK_DETECT
+ self._poke16(0, self._lmx2572_regs.get_reg(0))
+
+ return value_read
+
+
+ def _calculate_settings(self):
+ """
+ This function is intended to be called for calculating the register settings,
+ however it is implementation, not chip specific, so it is defined but not implemented
+ """
+ raise NotImplementedError("This function is meant to be overriden by a child class.")
+
+ def _set_default_values(self):
+ """
+ The register map has all base class defaults.
+ Subclasses can override this function to have the values populated on reset.
+ """
+ pass
+
+ def _pokes16(self, addr_vals):
+ """
+ Apply a series of pokes.
+ pokes16((0,1),(0,2)) is the same as calling poke16(0,1), poke16(0,2).
+ """
+ for addr, val in addr_vals:
+ self._poke16(addr, val)
+
+ def _set_output_a_enable(self, enable_output):
+ """
+ Sets output A (OUTA_PD)
+ """
+ new_value = lmx2572_regs_t.outa_pd_t.OUTA_PD_NORMAL_OPERATION if enable_output \
+ else lmx2572_regs_t.outa_pd_t.OUTA_PD_POWER_DOWN
+ self._lmx2572_regs.outa_pd = new_value
+
+ def _set_output_b_enable(self, enable_output):
+ """
+ Sets output B (OUTB_PD)
+ """
+ new_value = lmx2572_regs_t.outb_pd_t.OUTB_PD_NORMAL_OPERATION if enable_output \
+ else lmx2572_regs_t.outb_pd_t.OUTB_PD_POWER_DOWN
+ self._lmx2572_regs.outb_pd = new_value
+
+ def _set_output_a_power(self, power):
+ """
+ Sets the output A power
+ """
+ self._lmx2572_regs.outa_pwr = power & self._lmx2572_regs.outa_pwr_mask
+
+ def _set_output_b_power(self, power):
+ """
+ Sets the output B power
+ """
+ self._lmx2572_regs.outb_pwr = power & self._lmx2572_regs.outb_pwr_mask
+
+ def _set_fcal_hpfd_adj(self, phase_detector_frequency):
+ """
+ Sets the FCAL_HPFD_ADJ value based on the Fpfd
+ """
+ # These magic number frequency constants are from the data sheet
+ if phase_detector_frequency <= 37.5e6:
+ self._lmx2572_regs.fcal_hpfd_adj = 0x0
+ elif 37.5e6 < phase_detector_frequency <= 75e6:
+ self._lmx2572_regs.fcal_hpfd_adj = 0x1
+ elif 75e6 < phase_detector_frequency <= 100e6:
+ self._lmx2572_regs.fcal_hpfd_adj = 0x2
+ else: # 100MHz < phase_detector_frequency
+ self._lmx2572_regs.fcal_hpfd_adj = 0x3
+
+ def _set_fcal_lpfd_adj(self, phase_detector_frequency):
+ """
+ Sets the FCAL_LPFD_ADJ value based on the Fpfd
+ """
+ # These magic number frequency constants are from the data sheet
+ if phase_detector_frequency >= 10e6:
+ self._lmx2572_regs.fcal_lpfd_adj = 0x0
+ elif 10e6 > phase_detector_frequency >= 5e6:
+ self._lmx2572_regs.fcal_lpfd_adj = 0x1
+ elif 5e6 > phase_detector_frequency >= 2.5e6:
+ self._lmx2572_regs.fcal_lpfd_adj = 0x2
+ else: # phase_detector_frequency < 2.5MHz
+ self._lmx2572_regs.fcal_lpfd_adj = 0x3
+
+ def _set_pll_n(self, n):
+ """
+ Sets the pll_n registers
+ """
+ self._lmx2572_regs.pll_n_upper_3_bits = (n >> 16) & \
+ self._lmx2572_regs.pll_n_upper_3_bits_mask
+ self._lmx2572_regs.pll_n_lower_16_bits = n & self._lmx2572_regs.pll_n_lower_16_bits_mask
+
+ def _set_pll_den(self, den):
+ """
+ Sets the pll_den registers
+ """
+ self._lmx2572_regs.pll_den_upper = (den >> 16) & self._lmx2572_regs.pll_den_upper_mask
+ self._lmx2572_regs.pll_den_lower = den & self._lmx2572_regs.pll_den_lower_mask
+
+ def _set_mash_seed(self, mash_seed):
+ """
+ Sets the mash seed register
+ """
+ self._lmx2572_regs.mash_seed_upper = (mash_seed >> 16) & \
+ self._lmx2572_regs.mash_seed_upper_mask
+ self._lmx2572_regs.mash_seed_lower = mash_seed & self._lmx2572_regs.mash_seed_lower_mask
+
+ def _set_pll_num(self, num):
+ """
+ Sets the pll_num registers
+ """
+ self._lmx2572_regs.pll_num_upper = (num >> 16) & self._lmx2572_regs.pll_num_upper_mask
+ self._lmx2572_regs.pll_num_lower = num & self._lmx2572_regs.pll_num_lower_mask
+
+ def _set_mash_rst_count(self, mash_rst_count):
+ """
+ Sets the mash_rst_count registers
+ """
+ self._lmx2572_regs.mash_rst_count_upper = (mash_rst_count >> 16) & \
+ self._lmx2572_regs.mash_rst_count_upper_mask
+ self._lmx2572_regs.mash_rst_count_lower = mash_rst_count & \
+ self._lmx2572_regs.mash_rst_count_lower_mask
+
+ def _compute_and_set_mult_hi(self, reference_frequency):
+ multiplier_output_frequency = (reference_frequency*(int(self._lmx2572_regs.osc_2x.value)\
+ +1)*self._lmx2572_regs.mult) / self._lmx2572_regs.pll_r_pre
+ new_mult_hi = lmx2572_regs_t.mult_hi_t.MULT_HI_GREATER_THAN_100M \
+ if self._lmx2572_regs.mult > 1 and multiplier_output_frequency > 100e6 else \
+ lmx2572_regs_t.mult_hi_t.MULT_HI_LESS_THAN_EQUAL_TO_100M
+ self._lmx2572_regs.mult_hi = new_mult_hi
+
+ def _power_up_sequence(self):
+ """
+ Performs the intial register writes for the LMX2572
+ """
+ for register in reversed(range(NUMBER_OF_LMX2572_REGISTERS)):
+ if register in LMX2572.READ_ONLY_REGISTERS:
+ continue
+ self._poke16(register, self._lmx2572_regs.get_reg(register))
+
+ def _write_registers_frequency_tuning(self):
+ """
+ This function writes just the registers for frequency tuning
+ """
+ # Write PLL_N to registers R34 and R36
+ self._poke16(34, self._lmx2572_regs.get_reg(34))
+ self._poke16(36, self._lmx2572_regs.get_reg(36))
+ # Write PLL_DEN to registers R38 and R39
+ self._poke16(38, self._lmx2572_regs.get_reg(38))
+ self._poke16(39, self._lmx2572_regs.get_reg(39))
+ # Write PLL_NUM to registers R42 and R43
+ self._poke16(42, self._lmx2572_regs.get_reg(42))
+ self._poke16(43, self._lmx2572_regs.get_reg(43))
+
+ # MASH_SEED to registers R40 and R41
+ self._poke16(40, self._lmx2572_regs.get_reg(40))
+ self._poke16(41, self._lmx2572_regs.get_reg(41))
+
+ # Write OUTA_PWR to register R44 or OUTB_PWR to register R45
+ # Write OUTA_MUX to register R45 and/or OUTB_MUX to register R46
+ self._poke16(44, self._lmx2572_regs.get_reg(44))
+ self._poke16(45, self._lmx2572_regs.get_reg(45))
+ self._poke16(46, self._lmx2572_regs.get_reg(46))
+
+ # Write CHDIV to register R75
+ self._poke16(75, self._lmx2572_regs.get_reg(75))
+
+ # Write CPG to register R14
+ self._poke16(14, self._lmx2572_regs.get_reg(14))
+
+ # Write PFD_DLY_SEL to register R37
+ self._poke16(37, self._lmx2572_regs.get_reg(37))
+
+ # Write VCO_SEL to register R20
+ self._poke16(20, self._lmx2572_regs.get_reg(20))
+
+ # Write VCO_DACISET_STRT to register R17
+ self._poke16(17, self._lmx2572_regs.get_reg(17))
+
+ # Write VCO_CALCTRL_STRT to register R78
+ self._poke16(78, self._lmx2572_regs.get_reg(78))
+
+ # Write R0 to latch double buffered registers
+ self._poke16(0, self._lmx2572_regs.get_reg(0))
+ self._poke16(0, self._lmx2572_regs.get_reg(0))
+
+ def _write_registers_reference_chain(self):
+ """
+ This function writes the registers that are used for setting the reference chain
+ """
+ # Write FCAL_HPFD_ADJ to register R0
+ # Write FCAL_LPFD_ADJ to register R0
+ self._poke16(0, self._lmx2572_regs.get_reg(0))
+
+ # Write MULT_HI to register R9
+ # Write OSC_2X to register R9
+ self._poke16(9, self._lmx2572_regs.get_reg(9))
+
+ # Write MULT to register R10
+ self._poke16(10, self._lmx2572_regs.get_reg(10))
+
+ # Write PLL_R to register R11
+ self._poke16(11, self._lmx2572_regs.get_reg(11))
+ # Write PLL_R_PRE to register R12
+ self._poke16(12, self._lmx2572_regs.get_reg(12))
+
+ # if Phase SYNC being used:
+ # Write MASH_RST_COUNT to registers R69 and 70
+ if self.get_synchronization():
+ self._poke16(70, self._lmx2572_regs.get_reg(70))
+ self._poke16(69, self._lmx2572_regs.get_reg(69))
diff --git a/mpm/python/usrp_mpm/chips/max10_cpld_flash_ctrl.py b/mpm/python/usrp_mpm/chips/max10_cpld_flash_ctrl.py
new file mode 100644
index 000000000..c1e756124
--- /dev/null
+++ b/mpm/python/usrp_mpm/chips/max10_cpld_flash_ctrl.py
@@ -0,0 +1,235 @@
+#!/usr/bin/env python3
+#
+# Copyright 2020 Ettus Research, a National Instruments Brand
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+"""
+Update the CPLD image using the on-chip flash on Intel MAX10 devices
+"""
+import os
+import time
+
+class Max10CpldFlashCtrl():
+ """
+ Context manager class used to handle CPLD Flash reconfiguration.
+ Calling "with" using an instance of this class will disable write
+ protection for the duration of that context.
+ """
+ REVISION_REG = 0x0004
+ # Addresses relative to reconfiguration register offset
+ FLASH_STATUS_REG = 0x0000
+ FLASH_CONTROL_REG = 0x0004
+ FLASH_ADDR_REG = 0x0008
+ FLASH_WRITE_DATA_REG = 0x000C
+ FLASH_READ_DATA_REG = 0x0010
+ FLASH_CFM0_START_ADDR_REG = 0x0014
+ FLASH_CFM0_END_ADDR_REG = 0x0018
+
+ # Masks for FLASH_STATUS_REG
+ FLASH_MEM_INIT_ENABLED_MASK = 0x10000
+
+ def __init__(self, logger, regs, reconfig_regs_offset, cpld_min_revision):
+ if logger == None:
+ logger = get_logger('update_cpld')
+ self.log = logger
+ self.regs = regs
+ self.reconfig_regs_offset = reconfig_regs_offset
+ self.cpld_min_revision = cpld_min_revision
+
+ def peek32(self, addr):
+ return self.regs.peek32(addr + self.reconfig_regs_offset)
+
+ def poke32(self, addr, val):
+ self.regs.poke32(addr + self.reconfig_regs_offset, val)
+
+ def __enter__(self):
+ self.enabled_write_protection(enable=False)
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ self.enabled_write_protection(enable=True)
+
+ def enabled_write_protection(self, enable=True):
+ if enable:
+ self.poke32(self.FLASH_CONTROL_REG, 1 << 0) # FLASH_ENABLE_WP_STB
+ else:
+ self.poke32(self.FLASH_CONTROL_REG, 1 << 1) # FLASH_DISABLE_WP_STB
+
+ def check_revision(self):
+ self.log.debug('Checking for compatible CPLD revision')
+ cpld_revision = self.regs.peek32(self.REVISION_REG)
+ if cpld_revision < self.cpld_min_revision:
+ self.log.error("Unexpected CPLD revision 0x{:X}".format(cpld_revision))
+ return False
+ return True
+
+ def get_start_addr(self):
+ return self.peek32(self.FLASH_CFM0_START_ADDR_REG)
+
+ def get_end_addr(self):
+ return self.peek32(self.FLASH_CFM0_END_ADDR_REG)
+
+ def is_memory_initialization_enabled(self):
+ return self.peek32(self.FLASH_STATUS_REG) & self.FLASH_MEM_INIT_ENABLED_MASK
+
+ # expected value of 0x1110 indicates idle state of read, write, and erase
+ # routines (see function wait_for_idle for details), 0x0001 indicates the
+ # write protection is enabled
+ def check_reconfig_engine_status(self, expected_value=0x1111):
+ status = self.peek32(self.FLASH_STATUS_REG)
+ status = status & ~self.FLASH_MEM_INIT_ENABLED_MASK
+ if (status != expected_value):
+ self.log.error("Unexpected reconfig engine status 0x%08X" % status)
+ return False
+ return True
+
+ def wait_for_idle(self, operation, timeout=350):
+ """
+ Wait for the idle bit to assert for the given operation.
+ If the idle bit is not True before the timeout (given in ms),
+ return False.
+ """
+ if operation == 'write':
+ status_bit = 1 << 12 # FLASH_WRITE_IDLE
+ elif operation == 'erase':
+ status_bit = 1 << 8 # FLASH_ERASE_IDLE
+ elif operation == 'read':
+ status_bit = 1 << 4 # FLASH_READ_IDLE
+ else:
+ self.log.error('Cannot wait for unknown operation {}'.format(operation))
+ raise RuntimeError('Cannot wait for unknown operation {}'.format(operation))
+ for _ in range(0, timeout):
+ status = self.peek32(self.FLASH_STATUS_REG)
+ if (status & status_bit):
+ return True
+ time.sleep(0.001) # 1 ms
+ return False
+
+ def erase_flash_memory(self):
+ with self:
+ # check for sectors to be erased:
+ if self.is_memory_initialization_enabled():
+ sectors = [2, 3, 4]
+ else:
+ sectors = [4]
+ # erase each sector individually
+ for sector in sectors:
+ # start erase
+ self.poke32(self.FLASH_CONTROL_REG, (1 << 4) | ((sector & 0x7) << 5))
+ # wait for erase to finish
+ if not self.wait_for_idle('erase', timeout=350):
+ self.log.error('There was a timeout waiting for '
+ 'Flash erase to complete!')
+ return False
+ return True
+
+ def program_flash_memory(self, raw_data):
+ with self:
+ # write words one at a time
+ for i, data in enumerate(raw_data):
+ # status display
+ if (i%1000 == 0):
+ self.log.debug('%d%% written', i*4/self.file_size*100)
+ # write address and data
+ self.poke32(self.FLASH_ADDR_REG, self.cpld_start_address+i)
+ self.poke32(self.FLASH_WRITE_DATA_REG, data)
+ # start write operation
+ self.poke32(self.FLASH_CONTROL_REG, 1 << 3)
+ # wait for write to finish
+ if not self.wait_for_idle('write', timeout=2):
+ self.log.error('There was a timeout waiting for '
+ 'Flash write to complete!')
+ return False
+ if not self.check_reconfig_engine_status(expected_value=0x1110):
+ return False
+ return True
+
+ def verify_flash_memory(self, raw_data):
+ # read words one at a time
+ for i, data in enumerate(raw_data):
+ # status display
+ if (i%1000 == 0):
+ self.log.debug('%d%% done', i*4/self.file_size*100)
+ # write address
+ self.poke32(self.FLASH_ADDR_REG, self.cpld_start_address+i)
+ # start read operation
+ self.poke32(self.FLASH_CONTROL_REG, 1 << 2)
+ # wait for read to finish
+ if not self.wait_for_idle('read', timeout=1):
+ self.log.error('There was a timeout waiting for '
+ 'Flash read to complete!')
+ return False
+ # read data from device
+ device_data = self.peek32(self.FLASH_READ_DATA_REG)
+ if (data != device_data):
+ self.log.error("Data mismatch! address %d, expected value 0x%08X,"
+ " read value 0x%08X" %
+ (i+self.cpld_start_address, data, device_data))
+ return False
+ return True
+
+ def reverse_bits_in_byte(self, n):
+ result = 0
+ for _ in range(8):
+ result <<= 1
+ result |= n & 1
+ n >>= 1
+ return result
+
+ def update(self, filename):
+ if not self.check_revision():
+ return False
+
+ self.log.debug('Checking CPLD image file')
+ self.file_size = os.path.getsize(filename)
+ self.cpld_start_address = self.get_start_addr()
+ cpld_end_address = self.get_end_addr()
+ expected_size = (cpld_end_address+1-self.cpld_start_address)*4
+ if (self.file_size != expected_size):
+ self.log.error("Unexpected file size! Required size: %d bytes" % expected_size)
+ return False
+
+ # Convert data from bytes to 32-bit words and reverse bit order
+ # to be compatible with Altera's on-chip flash IP
+ raw_data = []
+ with open(filename, 'rb') as binary_file:
+ for _ in range(self.file_size//4):
+ number = 0
+ for _ in range(4):
+ number <<= 8
+ number |= self.reverse_bits_in_byte(int.from_bytes(binary_file.read(1), 'big'))
+ raw_data.append(number)
+
+ if not self.check_reconfig_engine_status():
+ return False
+
+ self.log.debug('Erasing CPLD flash memory...')
+ if not (self.erase_flash_memory()
+ and self.check_reconfig_engine_status()):
+ self.log.error('There was an error while reprogramming the CPLD image. '
+ 'Please program the CPLD again with a valid image before power '
+ 'cycling the board to ensure it is in a valid state.')
+ return False
+ self.log.debug('CPLD flash memory erased.')
+
+ self.log.debug('Programming flash memory...')
+ if not (self.program_flash_memory(raw_data)
+ and self.check_reconfig_engine_status()):
+ self.log.error('There was an error while reprogramming the CPLD image. '
+ 'Please program the CPLD again with a valid image before power '
+ 'cycling the board to ensure it is in a valid state.')
+ return False
+ self.log.debug('Flash memory programming complete.')
+
+ self.log.debug('Verifying image in flash...')
+ if not (self.verify_flash_memory(raw_data)
+ and self.check_reconfig_engine_status()):
+ self.log.error('There was an error while reprogramming the CPLD image. '
+ 'Please program the CPLD again with a valid image before power '
+ 'cycling the board to ensure it is in a valid state.')
+ return False
+ self.log.debug('Flash image verification complete.')
+
+ self.log.info('CPLD reprogrammed! Please power-cycle the device.')
+
+ return True
diff --git a/mpm/python/usrp_mpm/components.py b/mpm/python/usrp_mpm/components.py
index 5d1a31325..a87dbdce7 100644
--- a/mpm/python/usrp_mpm/components.py
+++ b/mpm/python/usrp_mpm/components.py
@@ -7,6 +7,7 @@
MPM Component management
"""
import os
+import re
import shutil
import subprocess
from usrp_mpm.rpc_server import no_rpc
@@ -30,6 +31,124 @@ class ZynqComponents(object):
###########################################################################
# Component updating
###########################################################################
+ def _log_and_raise(self, logstr):
+ self.log.error(logstr)
+ raise RuntimeError(logstr)
+
+ @classmethod
+ def _parse_dts_mpm_version_tag(cls, text):
+ """ parse a version line from the dts file. E.g.
+ "// mpm_version component1 3.4.5" will return
+ {"component1": (3, 4, 5)} """
+ dts_version_re = re.compile(r'^// mpm_version\s+(?P<comp>\S+)\s+(?P<ver>\S+)$')
+ match = dts_version_re.match(text)
+ if match is None:
+ return (None, None)
+
+ component = match[1]
+ version_list = [int(x, base=0) for x in match[2].split('.')]
+ return (component, tuple(version_list))
+
+ @classmethod
+ def _parse_dts_version_info_from_file(cls, filepath):
+ """
+ parse all version informations from dts file and store in dict
+ a c-style comment in the dts file like this
+ // mpm_version component1 3.4.5
+ // mpm_version other_component 3.5.0
+ will return a dict:
+ {"component1": (3, 4, 5), "other_component": (3, 5, 0)"}
+ """
+ suffix_current = "_current_version"
+ suffix_oldest = "_oldest_compatible_version"
+
+ version_info = {}
+ with open(filepath) as f:
+ text = f.read()
+ for line in text.splitlines():
+ component, version = cls._parse_dts_mpm_version_tag(line)
+ if not component:
+ continue
+ if component.endswith(suffix_oldest):
+ component = component[:-len(suffix_oldest)]
+ version_type = 'oldest'
+ elif component.endswith(suffix_current):
+ component = component[:-len(suffix_current)]
+ version_type = 'current'
+ else:
+ version_type = 'current'
+ if component not in version_info:
+ version_info[component] = {}
+ version_info[component][version_type] = version
+ return version_info
+
+ def _verify_compatibility(self, filebasename, update_dict):
+ """
+ check whether the given MPM compatibility matches the
+ version information stored in the FPGA DTS file
+ """
+ def _get_version_string(versions_enum):
+ version_strings = []
+ if 'current' in versions_enum:
+ version_strings.append("current: {}".format(
+ ".".join([str(x) for x in versions_enum['current']])))
+ if 'oldest' in versions_enum:
+ version_strings.append("oldest compatible: {}".format(
+ ".".join([str(x) for x in versions_enum['oldest']])))
+ return ', '.join(version_strings)
+
+
+ if update_dict.get('check_dts_for_compatibility'):
+ self.log.trace("Compatibility check MPM <-> FPGA via DTS enabled")
+ dtsfilepath = filebasename + '.dts'
+ if not os.path.exists(dtsfilepath):
+ self._log_and_raise("DTS file not found: {}".format(dtsfilepath))
+ self.log.trace("Parse DTS file {} for version information"\
+ .format(dtsfilepath))
+ fpga_versions = self._parse_dts_version_info_from_file(dtsfilepath)
+ if not fpga_versions:
+ self._log_and_raise("no component version information in DTS file")
+ if 'compatibility' not in update_dict:
+ self._log_and_raise("MPM FPGA version infos not found")
+ mpm_versions = update_dict['compatibility']
+ self.log.trace("DTS version infos: {}".format(fpga_versions))
+ self.log.trace("MPM version infos: {}".format(mpm_versions))
+
+ try:
+ for component in mpm_versions.keys():
+ # check for components that aren't available in the DTS file
+ if component in fpga_versions.keys():
+ self.log.trace(f"check compatibility for: FPGA-{component}")
+ mpm_version = mpm_versions[component]
+ fpga_version = fpga_versions[component]
+ self.log.trace("mpm_version: current: {}, compatible: {}".format(
+ mpm_version['current'], mpm_version['oldest']))
+ self.log.trace("fpga_version: current: {}, compatible: {}".format(
+ fpga_version['current'], fpga_version['oldest']))
+ if mpm_version['oldest'][0] > fpga_version['current'][0]:
+ error = "Component {} is too old ({}, MPM version: {})".format(
+ component,
+ _get_version_string(fpga_version),
+ _get_version_string(mpm_version))
+ self._log_and_raise(error)
+ elif mpm_version['current'][0] < fpga_version['oldest'][0]:
+ error = "Component {} is too new ({}, MPM version: {})".format(
+ component,
+ _get_version_string(fpga_version),
+ _get_version_string(mpm_version))
+ self._log_and_raise(error)
+ self.log.trace(f"Component {component} is good!")
+ else:
+ self.log.warning(f"component {component} defined in "\
+ f"MPM but not found in FPGA info, skipping.")
+ except RuntimeError as ex:
+ self._log_and_raise("MPM compatibility infos suggest that the "\
+ "new bitfile is not compatible, skipping installation. {}"\
+ .format(ex))
+ else:
+ self.log.trace("Compatibility check MPM <-> FPGA is disabled")
+ return
+
@no_rpc
def update_fpga(self, filepath, metadata):
"""
@@ -37,13 +156,19 @@ class ZynqComponents(object):
:param filepath: path to new FPGA image
:param metadata: Dictionary of strings containing metadata
"""
- self.log.trace("Updating FPGA with image at {} (metadata: `{}')"
- .format(filepath, str(metadata)))
- _, file_extension = os.path.splitext(filepath)
+ self.log.trace(f"Updating FPGA with image at {filepath}"\
+ " (metadata: `{str(metadata)}')")
+ file_name, file_extension = os.path.splitext(filepath)
+ self.log.trace("file_name: {}".format(file_name))
# Cut off the period from the file extension
file_extension = file_extension[1:].lower()
- binfile_path = self.updateable_components['fpga']['path'].format(
- self.device_info.get('product'))
+ if file_extension not in ['bit', 'bin']:
+ self._log_and_raise(f"Invalid FPGA bitfile: {filepath}")
+ binfile_path = self.updateable_components['fpga']['path']\
+ .format(self.device_info.get('product'))
+
+ self._verify_compatibility(file_name, self.updateable_components['fpga'])
+
if file_extension == "bit":
self.log.trace("Converting bit to bin file and writing to {}"
.format(binfile_path))
@@ -52,9 +177,7 @@ class ZynqComponents(object):
elif file_extension == "bin":
self.log.trace("Copying bin file to %s", binfile_path)
shutil.copy(filepath, binfile_path)
- else:
- self.log.error("Invalid FPGA bitfile: %s", filepath)
- raise RuntimeError("Invalid N3xx FPGA bitfile")
+
# RPC server will reload the periph manager after this.
return True
diff --git a/mpm/python/usrp_mpm/dboard_manager/CMakeLists.txt b/mpm/python/usrp_mpm/dboard_manager/CMakeLists.txt
index dfac467d0..b01d220df 100644
--- a/mpm/python/usrp_mpm/dboard_manager/CMakeLists.txt
+++ b/mpm/python/usrp_mpm/dboard_manager/CMakeLists.txt
@@ -28,9 +28,14 @@ set(USRP_MPM_DBMGR_FILES
${CMAKE_CURRENT_SOURCE_DIR}/magnesium_update_cpld.py
${CMAKE_CURRENT_SOURCE_DIR}/mg_init.py
${CMAKE_CURRENT_SOURCE_DIR}/mg_periphs.py
+ ${CMAKE_CURRENT_SOURCE_DIR}/zbx.py
${CMAKE_CURRENT_SOURCE_DIR}/test.py
${CMAKE_CURRENT_SOURCE_DIR}/unknown.py
${CMAKE_CURRENT_SOURCE_DIR}/dboard_iface.py
+ ${CMAKE_CURRENT_SOURCE_DIR}/x4xx_db_iface.py
+ ${CMAKE_CURRENT_SOURCE_DIR}/zbx_update_cpld.py
+ ${CMAKE_CURRENT_SOURCE_DIR}/x4xx_debug_db.py
+ ${CMAKE_CURRENT_SOURCE_DIR}/x4xx_if_test_cca.py
)
list(APPEND USRP_MPM_FILES ${USRP_MPM_DBMGR_FILES})
set(USRP_MPM_FILES ${USRP_MPM_FILES} PARENT_SCOPE)
diff --git a/mpm/python/usrp_mpm/dboard_manager/__init__.py b/mpm/python/usrp_mpm/dboard_manager/__init__.py
index 58262025e..28a8ef80b 100644
--- a/mpm/python/usrp_mpm/dboard_manager/__init__.py
+++ b/mpm/python/usrp_mpm/dboard_manager/__init__.py
@@ -14,6 +14,11 @@ if not __simulated__:
from .neon import Neon
from .e31x_db import E31x_db
from .eiscat import EISCAT
+ from .empty_slot import EmptySlot
+ from .zbx import ZBX
from .test import test
from .unknown import unknown
from .dboard_iface import DboardIface
+ from .x4xx_db_iface import X4xxDboardIface
+ from .x4xx_debug_db import X4xxDebugDboard
+ from .x4xx_if_test_cca import X4xxIfTestCCA
diff --git a/mpm/python/usrp_mpm/dboard_manager/base.py b/mpm/python/usrp_mpm/dboard_manager/base.py
index be37a6264..978f1c5ae 100644
--- a/mpm/python/usrp_mpm/dboard_manager/base.py
+++ b/mpm/python/usrp_mpm/dboard_manager/base.py
@@ -24,6 +24,9 @@ class DboardManagerBase(object):
# Very important: A list of PIDs that apply to the current device. Must be
# list, even if there's only one entry.
pids = []
+ # tuple of id and name of the first revision,
+ # id and name of revisions are consecutive (2, B), (3, C), ...
+ first_revision = (1, 'A')
# See PeriphManager.mboard_sensor_callback_map for a description.
rx_sensor_callback_map = {}
# See PeriphManager.mboard_sensor_callback_map for a description.
@@ -91,6 +94,13 @@ class DboardManagerBase(object):
"""
self.log.debug("deinit() called, but not implemented.")
+ def tear_down(self):
+ """
+ Tear down all members that need to be specially handled before
+ deconstruction.
+ """
+ pass
+
def get_serial(self):
"""
Return this daughterboard's serial number as a string. Will return an
@@ -98,6 +108,33 @@ class DboardManagerBase(object):
"""
return self.device_info.get("serial", "")
+ def get_revision(self):
+ """
+ Return this daughterboard's revision number as integer. Will return
+ -1 if no revision can be found or revision is not an integer
+ """
+ try:
+ return int(self.device_info.get('rev', '-1'))
+ except ValueError:
+ return -1
+
+ def get_revision_string(self):
+ """
+ Converts revision number to string.
+ """
+ return chr(ord(self.first_revision[1])
+ + self.get_revision()
+ - self.first_revision[0])
+
+ ##########################################################################
+ # Clocking
+ ##########################################################################
+ def reset_clock(self, value):
+ """
+ Called when the motherboard is reconfiguring its clocks.
+ """
+ pass
+
def update_ref_clock_freq(self, freq, **kwargs):
"""
Call this function if the frequency of the reference clock changes.
diff --git a/mpm/python/usrp_mpm/dboard_manager/dboard_iface.py b/mpm/python/usrp_mpm/dboard_manager/dboard_iface.py
index 87bff846b..e100b02a2 100755..100644
--- a/mpm/python/usrp_mpm/dboard_manager/dboard_iface.py
+++ b/mpm/python/usrp_mpm/dboard_manager/dboard_iface.py
@@ -28,6 +28,32 @@ class DboardIface(object):
if hasattr(self.mboard, 'log'):
self.log = self.mboard.log.getChild("DboardIface")
+ def tear_down(self):
+ """
+ Tear down all members that need to be specially handled before
+ deconstruction.
+ """
+ # The mboard object is the periph_manager that has the dboard
+ # that in turn has this DboardIface. Breaking the reference
+ # cycle will make garbage collection easier.
+ self.mboard = None
+
+ ####################################################################
+ # Power
+ # Enable and disable the DB's power rails
+ ####################################################################
+ def enable_daughterboard(self, enable = True):
+ """
+ Enable or disable the daughterboard.
+ """
+ raise NotImplementedError('DboardIface::enable_daughterboard() not supported!')
+
+ def check_enable_daughterboard(self):
+ """
+ Return the enable state of the daughterboard.
+ """
+ raise NotImplementedError('DboardIface::check_enable_daughterboard() not supported!')
+
####################################################################
# CTRL SPI
# CTRL SPI lines are connected to the CPLD of the DB if it exists
@@ -42,18 +68,6 @@ class DboardIface(object):
raise NotImplementedError('DboardIface::ctrl_spi_reset() not supported!')
####################################################################
- # GPIO
- # GPIO lines are used for high speed control of the DB
- ####################################################################
- def get_high_speed_gpio_ctrl_core(self):
- """
- Return a GpioAtrCore4000 instance that controls the GPIO lines
- interfacing the MB and DB
- """
- raise NotImplementedError('DboardIface::get_high_speed_gpio_ctrl_core()'
- ' not supported!')
-
- ####################################################################
# Management Bus
####################################################################
@@ -82,20 +96,28 @@ class DboardIface(object):
"""
raise NotImplementedError('DboardIface::set_if_freq() not supported!')
+ def get_if_freq(self, direction, channel):
+ """
+ Gets the IF frequency of the ADC/DAC corresponding
+ to the specified channel of the DB.
+ """
+ raise NotImplementedError('DboardIface::get_if_freq() not supported!')
+
+ def enable_iq_swap(self, enable, direction, channel):
+ """
+ Enable or disable swap of I and Q samples from the RFDCs.
+ """
+ raise NotImplementedError('DboardIface::enable_iq_swap() not supported!')
+
+ def get_sample_rate(self):
+ """
+ Gets the sample rate of the RFDCs.
+ """
+ raise NotImplementedError('DboardIface::get_sample_rate() not supported!')
+
def get_prc_rate(self):
"""
Returns the rate of the PLL Reference Clock (PRC) which is
routed to the daughterboard.
"""
raise NotImplementedError('DboardIface::get_pll_ref_clock() not supported!')
-
- ####################################################################
- # SPCC MPCC Control
- ####################################################################
- def get_protocol_cores(self):
- """
- Returns all discovered protocols in SPCC and MPCC blocks on the
- Daughterboard's CPLD in the form of SpiCore4000, I2cCore4000,
- UartCore4000, and GpioAtrCore4000
- """
- raise NotImplementedError('DboardIface::get_protocol_cores() not supported!')
diff --git a/mpm/python/usrp_mpm/dboard_manager/empty_slot.py b/mpm/python/usrp_mpm/dboard_manager/empty_slot.py
new file mode 100644
index 000000000..997d3ac6a
--- /dev/null
+++ b/mpm/python/usrp_mpm/dboard_manager/empty_slot.py
@@ -0,0 +1,37 @@
+#
+# Copyright 2021 Ettus Research, a National Instruments Brand
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+"""
+Dummy daughterboard class for empty slots
+"""
+from usrp_mpm.dboard_manager import DboardManagerBase
+from usrp_mpm.mpmlog import get_logger
+
+class EmptySlot(DboardManagerBase):
+ """
+ DboardManager class for when a slot is empty
+ """
+ #########################################################################
+ # Overridables
+ #
+ # See DboardManagerBase for documentation on these fields
+ #########################################################################
+ pids = [0x0]
+ ### End of overridables #################################################
+
+ def __init__(self, slot_idx, **kwargs):
+ DboardManagerBase.__init__(self, slot_idx, **kwargs)
+ self.log = get_logger("EmptyDB-{}".format(slot_idx))
+ self.log.trace("Initializing Empty dboard, slot index %d",
+ self.slot_idx)
+
+ def init(self, args):
+ """
+ Execute necessary init dance to bring up dboard
+ """
+ self.log.debug("init() called with args `{}'".format(
+ ",".join(['{}={}'.format(x, args[x]) for x in args])
+ ))
+ return True
diff --git a/mpm/python/usrp_mpm/dboard_manager/x4xx_db_iface.py b/mpm/python/usrp_mpm/dboard_manager/x4xx_db_iface.py
new file mode 100644
index 000000000..d89236859
--- /dev/null
+++ b/mpm/python/usrp_mpm/dboard_manager/x4xx_db_iface.py
@@ -0,0 +1,144 @@
+#
+# Copyright 2019 Ettus Research, a National Instruments Brand
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+
+from usrp_mpm.sys_utils.db_flash import DBFlash
+from usrp_mpm.sys_utils.gpio import Gpio
+from usrp_mpm.dboard_manager import DboardIface
+from usrp_mpm import lib # Pulls in everything from C++-land
+
+class X4xxDboardIface(DboardIface):
+ """
+ X4xx DboardIface implementation
+
+ slot_idx - The numerical ID of the daughterboard slot using this
+ interface (e.g. 0, 1)
+ motherboard - The instance of the motherboard class which implements
+ these controls
+ """
+ # The device tree label for the bus to the DB's Management EEPROM
+ MGMT_EEPROM_DEVICE_LABEL = "e0004000.i2c"
+
+ def __init__(self, slot_idx, motherboard):
+ super().__init__(slot_idx, motherboard)
+ self.db_cpld_iface = motherboard.ctrlport_regs.get_db_cpld_iface(self.slot_idx)
+ self._power_enable = Gpio('DB{}_PWR_EN'.format(slot_idx), Gpio.OUTPUT)
+ self._power_status = Gpio('DB{}_PWR_STATUS'.format(slot_idx), Gpio.INPUT)
+
+ self.db_flash = DBFlash(slot_idx, log=self.log)
+
+ def tear_down(self):
+ self.log.trace("Tearing down X4xx daughterboard...")
+ if self.db_flash:
+ self.db_flash.deinit()
+ super().tear_down()
+
+ ####################################################################
+ # Power
+ # Enable and disable the DB's power rails
+ ####################################################################
+ def enable_daughterboard(self, enable=True):
+ """
+ Enable or disable the daughterboard.
+ """
+ if self.db_flash and not enable:
+ self.db_flash.deinit()
+ self._power_enable.set(enable)
+ self.mboard.cpld_control.enable_daughterboard(self.slot_idx, enable)
+ if self.db_flash and enable:
+ self.db_flash.init()
+
+ def check_enable_daughterboard(self):
+ """
+ Return the enable state of the daughterboard.
+ """
+ return self._power_status.get()
+
+ ####################################################################
+ # CTRL SPI
+ # CTRL SPI lines are connected to the CPLD of the DB if it exists
+ ####################################################################
+ def peek_db_cpld(self, addr):
+ return self.db_cpld_iface.peek32(addr)
+
+ def poke_db_cpld(self, addr, val):
+ self.db_cpld_iface.poke32(addr, val)
+
+ ####################################################################
+ # Management Bus
+ ####################################################################
+
+ ####################################################################
+ # Calibration SPI
+ # The SPI/QSPI node used to interact with the DB
+ # Calibration EEPROM if it exists
+ ####################################################################
+ def get_cal_eeprom_spi_node(self, addr):
+ """
+ Returns the QSPI node leading to the calibration EEPROM of the
+ given DB.
+ """
+ chip_select = self.mboard.qspi_cs.get(self.db_name, None)
+ if chip_select is None:
+ raise RuntimeError('No QSPI chip select corresponds ' \
+ 'with daughterboard {}'.format(self.db_name))
+ return self.mboard.qspi_nodes[chip_select]
+
+ ####################################################################
+ # MB Control
+ # Some of the MB settings may be controlled from the DB Driver
+ ####################################################################
+ def _find_converters(self, direction='both', channel='both'):
+ """
+ Returns a list of (tile_id, block_id, is_dac) tuples describing
+ the data converters associated with a given channel and direction.
+ """
+ return self.mboard.rfdc._find_converters(self.slot_idx, direction, channel)
+
+ def set_if_freq(self, freq, direction='both', channel='both'):
+ """
+ Use the rfdc_ctrl object to set the IF frequency of the ADCs and
+ DACs corresponding to the specified channels of the DB.
+ By default, all channels and directions will be set.
+ Returns True if the IF frequency was successfully set.
+ """
+ for tile_id, block_id, is_dac in self._find_converters(direction, channel):
+ if not self.mboard.rfdc._rfdc_ctrl.set_if(tile_id, block_id, is_dac, freq):
+ return False
+ return True
+
+ def get_if_freq(self, direction, channel):
+ """
+ Gets the IF frequency of the ADC/DAC corresponding
+ to the specified channel of the DB.
+ """
+ converters = self._find_converters(direction, channel)
+ assert len(converters) == 1, \
+ 'Expected a single RFDC associated with {}{}. Instead found {}.' \
+ .format(direction, channel, len(converters))
+ (tile_id, block_id, is_dac) = converters[0]
+ return self.mboard.rfdc._rfdc_ctrl.get_nco_freq(tile_id, block_id, is_dac)
+
+ def enable_iq_swap(self, enable, direction, channel):
+ """
+ Enable or disable swap of I and Q samples from the RFDCs.
+ """
+ for tile_id, block_id, is_dac in self._find_converters(direction, channel):
+ self.mboard.rfdc._rfdc_regs.enable_iq_swap(enable, self.slot_idx, block_id, is_dac)
+
+ def get_sample_rate(self):
+ """
+ Gets the sample rate of the RFDCs.
+ """
+ return self.mboard.get_spll_freq()
+
+ def get_prc_rate(self):
+ """
+ Returns the rate of the PLL Reference Clock (PRC) which is
+ routed to the daughterboard.
+ Note: The ref clock will change if the sample clock frequency
+ is modified.
+ """
+ return self.mboard.get_prc_rate()
diff --git a/mpm/python/usrp_mpm/dboard_manager/x4xx_debug_db.py b/mpm/python/usrp_mpm/dboard_manager/x4xx_debug_db.py
new file mode 100644
index 000000000..f5e023229
--- /dev/null
+++ b/mpm/python/usrp_mpm/dboard_manager/x4xx_debug_db.py
@@ -0,0 +1,152 @@
+#
+# Copyright 2020 Ettus Research, a National Instruments Brand
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+"""
+Debug dboard implementation module
+"""
+
+from usrp_mpm.dboard_manager import DboardManagerBase
+from usrp_mpm.mpmlog import get_logger
+from usrp_mpm.sys_utils.gpio import Gpio
+
+class DebugDboardSignalPath:
+ def __init__(self, slot_idx, path, adc_indexes, dac_indexes, loopback):
+ self.log = get_logger("X4xxDebugDboard-{}-path-{}".format(slot_idx, path))
+
+ self.rxa2_led = Gpio("DB{}_RX{}2_LED".format(slot_idx, path), Gpio.OUTPUT, 0)
+ self.rxa_led = Gpio("DB{}_RX{}_LED".format(slot_idx, path), Gpio.OUTPUT, 0)
+ self.txa_led = Gpio("DB{}_TX{}_LED".format(slot_idx, path), Gpio.OUTPUT, 0)
+
+ self.trx_ctrl = Gpio("DB{}_TRX{}_CTRL".format(slot_idx, path), Gpio.OUTPUT, 0)
+ self.rx_mux_ctrl = Gpio("DB{}_RX{}_MUX_CTRL".format(slot_idx, path), Gpio.OUTPUT, 0)
+ self.tx_mux_ctrl = Gpio("DB{}_TX{}_MUX_CTRL".format(slot_idx, path), Gpio.OUTPUT, 0)
+
+ self._adc_indices = adc_indexes
+ self._dac_indices = dac_indexes
+ self._loopback = loopback
+ self._path = path
+
+ def configure(self, adc, dac, loopback):
+ """
+ Configure this path with the appropriate settings
+ """
+ if adc.lower() not in self._adc_indices:
+ error_msg = "Could not find ADC {} on path {}. Possible ADCs: {}".format(
+ adc, self._path, ", ".join(self._adc_indices.keys())
+ )
+ self.log.error(error_msg)
+ raise RuntimeError(error_msg)
+
+ if dac.lower() not in self._dac_indices:
+ error_msg = "Could not find DAC {} on path {}. Possible DACs: {}".format(
+ dac, self._path, ", ".join(self._dac_indices.keys())
+ )
+ self.log.error(error_msg)
+ raise RuntimeError(error_msg)
+
+ self.rx_mux_ctrl.set(self._adc_indices[adc.lower()])
+ self.tx_mux_ctrl.set(self._dac_indices[dac.lower()])
+ self.trx_ctrl.set(self._loopback if loopback else not self._loopback)
+
+
+class X4xxDebugDboard(DboardManagerBase):
+ """
+ Holds all dboard specific information and methods of the X4xx debug dboard
+ """
+ #########################################################################
+ # Overridables
+ #
+ # See DboardManagerBase for documentation on these fields
+ #########################################################################
+ pids = [0x4001]
+ ### End of overridables #################################################
+
+ def __init__(self, slot_idx, **kwargs):
+ DboardManagerBase.__init__(self, slot_idx, **kwargs)
+ self.log = get_logger("X4xxDebugDboard-{}".format(slot_idx))
+ self.log.trace("Initializing X4xxDebug daughterboard, slot index %d",
+ self.slot_idx)
+
+ # Interface with MB HW
+ if 'db_iface' not in kwargs:
+ self.log.error("Required DB Iface was not provided!")
+ raise RuntimeError("Required DB Iface was not provided!")
+ self.db_iface = kwargs['db_iface']
+
+ # Power on the card
+ self.db_iface.enable_daughterboard(enable=True)
+ if not self.db_iface.check_enable_daughterboard():
+ self.db_iface.enable_daughterboard(enable=False)
+ self.log.error('Debug dboard {} power up failed'.format(self.slot_idx))
+ raise RuntimeError('Debug dboard {} power up failed'.format(self.slot_idx))
+
+ self._paths = {
+ "a": DebugDboardSignalPath(
+ slot_idx,
+ "A",
+ {
+ "adc0": 1,
+ "adc2": 0,
+ },
+ {
+ "dac0": 1,
+ "dac2": 0,
+ },
+ 1 # TRXA_CTRL=1 enables loopback
+ ),
+ "b": DebugDboardSignalPath(
+ slot_idx,
+ "B",
+ {
+ "adc3": 1,
+ "adc1": 0,
+ },
+ {
+ "dac3": 1,
+ "dac1": 0,
+ },
+ 0 # TRXB_CTRL=0 enables loopback
+ ),
+ }
+
+
+ # TODO: Configure the correct RFDC settings for this board
+ #if not self.db_iface.disable_mixer():
+ # raise RuntimeError("Received an error disabling the mixer for slot_idx={}".format(slot_idx))
+
+ def init(self, args):
+ """
+ Execute necessary init dance to bring up dboard
+ """
+ self.log.debug("init() called with args `{}'".format(
+ ",".join(['{}={}'.format(x, args[x]) for x in args])
+ ))
+ self.config_path("a", "adc0", "dac0", 0)
+ self.config_path("b", "adc1", "dac1", 0)
+ return True
+
+ def deinit(self):
+ pass
+
+ def tear_down(self):
+ self.db_iface.tear_down()
+
+ def config_path(self, path, adc, dac, loopback):
+ """
+ Configure the signal paths on the daughterboard.
+ path - Select between front panel connectors A or B. The two paths are unconnected.
+ adc - Select which ADC to connect to the path (adc0 or adc2 on A, adc1 or adc3 on B)
+ dac - Select which DAC to connect to the path (dac0 or dac2 on A, dac1 or dac3 on B)
+ loopback - Whether to enable loopback (1) or route the ADC/DACs to the front panel (0)
+
+ Example MPM shell usage:
+ > db_0_config_path a adc0 dac2 1
+ """
+ if path.lower() not in self._paths:
+ self.log.error("Tried to configure path {} which does not exist!".format(path))
+ raise RuntimeError("Tried to configure path {} which does not exist!".format(path))
+
+ path = self._paths[path.lower()]
+ path.configure(adc, dac, int(loopback))
diff --git a/mpm/python/usrp_mpm/dboard_manager/x4xx_if_test_cca.py b/mpm/python/usrp_mpm/dboard_manager/x4xx_if_test_cca.py
new file mode 100644
index 000000000..c435ddb1c
--- /dev/null
+++ b/mpm/python/usrp_mpm/dboard_manager/x4xx_if_test_cca.py
@@ -0,0 +1,167 @@
+#
+# Copyright 2020 Ettus Research, a National Instruments Brand
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+"""
+IF Test CCA implementation module
+"""
+from usrp_mpm.dboard_manager import DboardManagerBase
+from usrp_mpm.mpmlog import get_logger
+from usrp_mpm.sys_utils.gpio import Gpio
+
+class X4xxIfTestCCA(DboardManagerBase):
+ """
+ Holds all dboard specific information and methods of the X4xx IF Test CCA
+ """
+ #########################################################################
+ # Overridables
+ #
+ # See DboardManagerBase for documentation on these fields
+ #########################################################################
+ pids = [0x4006]
+ ### End of overridables #################################################
+
+ def __init__(self, slot_idx, **kwargs):
+ DboardManagerBase.__init__(self, slot_idx, **kwargs)
+ self.log = get_logger("X4xxIfTestCCA-{}".format(slot_idx))
+ self.log.trace("Initializing X4xxIfTestCCA, slot index %d",
+ self.slot_idx)
+
+ # Interface with MB HW
+ if 'db_iface' not in kwargs:
+ self.log.error("Required DB Iface was not provided!")
+ raise RuntimeError("Required DB Iface was not provided!")
+ self.db_iface = kwargs['db_iface']
+
+ # Power on the card
+ self.db_iface.enable_daughterboard(enable=True)
+ if not self.db_iface.check_enable_daughterboard():
+ self.db_iface.enable_daughterboard(enable=False)
+ self.log.error('IF Test CCA {} power up failed'.format(self.slot_idx))
+ raise RuntimeError('IF Test CCA {} power up failed'.format(self.slot_idx))
+
+ # [boolean for stage 1 mux , boolean for stage 2 mux]
+ self._adc_mux_settings = {
+ "adc0" : [0, 0],
+ "adc1" : [1, 1],
+ "adc2" : [1, 0],
+ "adc3" : [0, 1],
+ }
+
+ self._dac_mux_settings = {
+ "dac0" : [1, 0],
+ "dac1" : [1, 1],
+ "dac2" : [0, 0],
+ "dac3" : [0, 1],
+ }
+
+ # There are 4 possible Tx (DAC) streams that are available to choose
+ # to export to the SMA TX port using a 2-stage hardware mux.
+
+ # Choose between 0 and 2 OR 1 and 3
+ self.tx_0_2_1_3_mux_ctrl = Gpio("DB{}_TX0_2p_1_3n".format(slot_idx), Gpio.OUTPUT, 0)
+ # Choose between 0 OR 2
+ self.tx_0_2_mux_ctrl = Gpio("DB{}_TX_MUX_0p_2n".format(slot_idx), Gpio.OUTPUT, 0)
+ # Choose between 1 OR 3
+ self.tx_1_3_mux_ctrl = Gpio("DB{}_TX_MUX_1p_3n".format(slot_idx), Gpio.OUTPUT, 0)
+
+ # The signal from the SMA RX port can be directed to one of the 4
+ # available Rx (ADC) streams using a 2-stage hardware mux.
+
+ # Choose between 0 and 2 OR 1 and 3
+ self.rx_0_2_1_3_mux_ctrl = Gpio("DB{}_RX0_2p_1_3n".format(slot_idx), Gpio.OUTPUT, 0)
+ # Choose between 0 OR 2
+ self.rx_0_2_mux_ctrl = Gpio("DB{}_RX_MUX_0p_2n".format(slot_idx), Gpio.OUTPUT, 0)
+ # Choose between 1 OR 3
+ self.rx_1_3_mux_ctrl = Gpio("DB{}_RX_MUX_1p_3n".format(slot_idx), Gpio.OUTPUT, 0)
+
+ self._tx_path = ""
+ self._rx_path = ""
+
+ # Controls to load the power supplies on the daughterboard. Enabling
+ # these will increase the power draw of the daughterboard.
+ self.enable_1v8_load = Gpio("DB{}_1V8_LOAD".format(slot_idx), Gpio.OUTPUT, 0)
+ self.enable_2v5_load = Gpio("DB{}_2V5_LOAD".format(slot_idx), Gpio.OUTPUT, 0)
+ self.enable_3v3_load = Gpio("DB{}_3V3_LOAD".format(slot_idx), Gpio.OUTPUT, 0)
+ self.enable_3v3_mcu_load = Gpio("DB{}_3V3_MCU_LOAD".format(slot_idx), Gpio.OUTPUT, 0)
+ self.enable_3v7_load = Gpio("DB{}_3V7_LOAD".format(slot_idx), Gpio.OUTPUT, 0)
+ self.enable_12v_load = Gpio("DB{}_12V_LOAD".format(slot_idx), Gpio.OUTPUT, 0)
+
+ # Control to choose between DAC output or MB VCM signals as the VCM
+ # signal to use on board.
+ self.disable_vcm_dac = Gpio("DB{}_VCM_MB_nDAC".format(slot_idx), Gpio.OUTPUT, 0)
+
+ # Control to choose which MB clock to output to the SMA Clock port.
+ # Choices are BaseRefClk and PllRefClk
+ self.disable_vcm_dac = Gpio("DB{}_REF_CLK_SEL_USR".format(slot_idx), Gpio.OUTPUT, 0)
+
+
+ def init(self, args):
+ """
+ Execute necessary init dance to bring up dboard
+ """
+ self.log.debug("init() called with args `{}'".format(
+ ",".join(['{}={}'.format(x, args[x]) for x in args])
+ ))
+ self.config_tx_path("dac0")
+ self.config_rx_path("adc0")
+ return True
+
+ def deinit(self):
+ pass
+
+ def tear_down(self):
+ self.db_iface.tear_down()
+
+ def config_tx_path(self, dac):
+ """
+ Configure the tx signal path on the daughterboard.
+ dac - Select which DAC to connect to the Tx path (dac0 through dac3)
+
+ Example MPM shell usage:
+ > db_0_config_tx_path dac2
+ """
+
+ if dac.lower() not in self._dac_mux_settings:
+ error_msg = "Could not find DAC {}. Possible DACs: {}".format(
+ dac, ", ".join(self._dac_mux_settings.keys())
+ )
+ self.log.error(error_msg)
+ raise RuntimeError(error_msg)
+
+ # Only one of the following setting really matters; simplify logic
+ # by toggling both since the stage 2 decides what gets connected.
+ self.tx_0_2_mux_ctrl.set(self._dac_mux_settings[dac.lower()][0])
+ self.tx_1_3_mux_ctrl.set(self._dac_mux_settings[dac.lower()][0])
+ self.tx_0_2_1_3_mux_ctrl.set(self._dac_mux_settings[dac.lower()][1])
+ self._tx_path = dac.upper()
+
+ def get_tx_path(self):
+ return self._tx_path
+
+ def config_rx_path(self, adc):
+ """
+ Configure the rx signal path on the daughterboard.
+ adc - Select which ADC to connect to the Rx path (adc0 through adc3)
+
+ Example MPM shell usage:
+ > db_0_config_rx_path adc0
+ """
+
+ if adc.lower() not in self._adc_mux_settings:
+ error_msg = "Could not find ADC {}. Possible ADCs: {}".format(
+ adc, ", ".join(self._adc_mux_settings.keys())
+ )
+ self.log.error(error_msg)
+ raise RuntimeError(error_msg)
+
+ self.rx_0_2_1_3_mux_ctrl.set(self._adc_mux_settings[adc.lower()][1])
+ # Only one of the following setting really matters; simplify logic
+ # by toggling both
+ self.rx_0_2_mux_ctrl.set(self._adc_mux_settings[adc.lower()][0])
+ self.rx_1_3_mux_ctrl.set(self._adc_mux_settings[adc.lower()][0])
+ self._rx_path = adc.upper()
+
+ def get_rx_path(self):
+ return self._rx_path
diff --git a/mpm/python/usrp_mpm/dboard_manager/zbx.py b/mpm/python/usrp_mpm/dboard_manager/zbx.py
new file mode 100644
index 000000000..8343119c8
--- /dev/null
+++ b/mpm/python/usrp_mpm/dboard_manager/zbx.py
@@ -0,0 +1,461 @@
+#
+# Copyright 2019-2020 Ettus Research, a National Instruments Brand
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+"""
+ZBX dboard implementation module
+"""
+
+import time
+from usrp_mpm import tlv_eeprom
+from usrp_mpm.dboard_manager import DboardManagerBase
+from usrp_mpm.mpmlog import get_logger
+from usrp_mpm.chips.ic_reg_maps import zbx_cpld_regs_t
+from usrp_mpm.periph_manager.x4xx_periphs import get_temp_sensor
+from usrp_mpm.sys_utils.udev import get_eeprom_paths_by_symbol
+
+###############################################################################
+# Helpers
+###############################################################################
+def parse_encoded_git_hash(encoded):
+ """
+ Helper function: Unpacks the git hash encoded in the ZBX CPLD image into
+ the git hash and a dirty flag.
+ """
+ git_hash = encoded & 0x0FFFFFFF
+ tree_dirty = ((encoded & 0xF0000000) > 0)
+ dirtiness_qualifier = 'dirty' if tree_dirty else 'clean'
+ return (git_hash, dirtiness_qualifier)
+
+
+# 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']),
+ }
+
+
+###############################################################################
+# Main dboard control class
+###############################################################################
+class ZBX(DboardManagerBase):
+ """
+ Holds all dboard specific information and methods of the ZBX dboard
+ """
+ #########################################################################
+ # Overridables
+ #
+ # See DboardManagerBase for documentation on these fields
+ #########################################################################
+ pids = [0x4002]
+ rx_sensor_callback_map = {
+ 'temperature': 'get_rf_temp_sensor',
+ }
+ tx_sensor_callback_map = {
+ 'temperature': 'get_rf_temp_sensor',
+ }
+ ### End of overridables #################################################
+
+ # Daughterboard required rev_compat value, this is compared against
+ # rev_compat in the eeprom
+ # Change only on breaking changes
+ DBOARD_REQUIRED_COMPAT_REV = 0x1
+
+ # CPLD compatibility revision
+ # Change this revision only on breaking changes.
+ REQ_OLDEST_COMPAT_REV = 0x20110611
+ REQ_COMPAT_REV = 0x20110611
+
+ #########################################################################
+ # MPM Initialization
+ #########################################################################
+ def __init__(self, slot_idx, **kwargs):
+ DboardManagerBase.__init__(self, slot_idx, **kwargs)
+ self.log = get_logger("ZBX-{}".format(slot_idx))
+ self.log.trace("Initializing ZBX daughterboard, slot index %d",
+ self.slot_idx)
+
+ # local variable to track if PLL ref clock is enabled for the CPLD logic
+ self._clock_enabled = False
+
+ # Interface with MB HW
+ if 'db_iface' not in kwargs:
+ self.log.error("Required DB Iface was not provided!")
+ raise RuntimeError("Required DB Iface was not provided!")
+ self.db_iface = kwargs['db_iface']
+
+ self.eeprom_symbol = f"db{slot_idx}_eeprom"
+ eeprom = self._get_eeprom()
+ if eeprom["rev_compat"] != self.DBOARD_REQUIRED_COMPAT_REV:
+ err = f"Found ZBX rev_compat 0x{eeprom['rev_compat']:02x}," \
+ f" required is 0x{self.DBOARD_REQUIRED_COMPAT_REV:02x}"
+ self.log.error(err)
+ raise RuntimeError(err)
+
+ # Initialize daughterboard CPLD control
+ self.poke_cpld = self.db_iface.poke_db_cpld
+ self.peek_cpld = self.db_iface.peek_db_cpld
+ self.regs = zbx_cpld_regs_t()
+ self._spi_addr = self.regs.SPI_READY_addr
+ self._enable_base_power()
+ # Check register map compatibility
+ self._check_compat_version()
+ self.log.debug("ZBX CPLD build git hash: %s", self._get_cpld_git_hash())
+ # Power up the DB
+ self._enable_power()
+ # enable PLL reference clock
+ self.reset_clock(False)
+ self._cpld_set_safe_defaults()
+
+ def _get_eeprom(self):
+ """
+ Return the eeprom data.
+ """
+ path = get_eeprom_paths_by_symbol(self.eeprom_symbol)[self.eeprom_symbol]
+ eeprom, _ = tlv_eeprom.read_eeprom(path, EepromTagMap.tagmap, EepromTagMap.magic, None)
+ return eeprom
+
+ def _enable_base_power(self, enable=True):
+ """
+ Enables or disables power to the DB which enables communication to DB CPLD
+ """
+ if enable:
+ self.db_iface.enable_daughterboard(enable=True)
+ if not self.db_iface.check_enable_daughterboard():
+ self.db_iface.enable_daughterboard(enable=False)
+ self.log.error('ZBX {} power up failed'.format(self.slot_idx))
+ raise RuntimeError('ZBX {} power up failed'.format(self.slot_idx))
+ else: # disable
+ # Removing power from the CPLD will set all the the output pins to open and the
+ # supplies default to disabled on power up.
+ self.db_iface.enable_daughterboard(enable=False)
+ if self.db_iface.check_enable_daughterboard():
+ self.log.error('ZBX {} power down failed'.format(self.slot_idx))
+
+ def _enable_power(self, enable=True):
+ """ Enables or disables power switches internal to the DB CPLD """
+ self.regs.ENABLE_TX_POS_7V0 = self.regs.ENABLE_TX_POS_7V0_t(int(enable))
+ self.regs.ENABLE_RX_POS_7V0 = self.regs.ENABLE_RX_POS_7V0_t(int(enable))
+ self.regs.ENABLE_POS_3V3 = self.regs.ENABLE_POS_3V3_t(int(enable))
+ self.poke_cpld(
+ self.regs.ENABLE_POS_3V3_addr,
+ self.regs.get_reg(self.regs.ENABLE_POS_3V3_addr))
+
+ def _check_compat_version(self):
+ """ Check compatibility of DB CPLD image and SW regmap """
+ compat_revision_addr = self.regs.OLDEST_COMPAT_REVISION_addr
+ cpld_oldest_compat_revision = self.peek_cpld(compat_revision_addr)
+ if cpld_oldest_compat_revision < self.REQ_OLDEST_COMPAT_REV:
+ err_msg = (
+ f'DB CPLD oldest compatible revision 0x{cpld_oldest_compat_revision:x}'
+ f' is out of date, the required revision is 0x{self.REQ_OLDEST_COMPAT_REV:x}. '
+ f'Update your CPLD image.')
+ self.log.error(err_msg)
+ raise RuntimeError(err_msg)
+ if cpld_oldest_compat_revision > self.REQ_OLDEST_COMPAT_REV:
+ err_msg = (
+ f'DB CPLD oldest compatible revision 0x{cpld_oldest_compat_revision:x}'
+ f' is newer than the expected revision 0x{self.REQ_OLDEST_COMPAT_REV:x}.'
+ ' Downgrade your CPLD image or update MPM.')
+ self.log.error(err_msg)
+ raise RuntimeError(err_msg)
+
+ if not self.has_compat_version(self.REQ_COMPAT_REV):
+ err_msg = (
+ "ZBX DB CPLD revision is too old. Update your"
+ f" CPLD image to at least 0x{self.REQ_COMPAT_REV:08x}.")
+ self.log.error(err_msg)
+ raise RuntimeError(err_msg)
+
+ def has_compat_version(self, min_required_version):
+ """
+ Check for a minimum required version.
+ """
+ cpld_image_compat_revision = self.peek_cpld(self.regs.REVISION_addr)
+ return cpld_image_compat_revision >= min_required_version
+
+ # pylint: disable=too-many-statements
+ def _cpld_set_safe_defaults(self):
+ """
+ Set the CPLD into a safe state.
+ """
+ cpld_regs = zbx_cpld_regs_t()
+ # We un-configure some registers to force a change later. None of these
+ # values get written to the CPLD!
+ cpld_regs.RF0_OPTION = cpld_regs.RF0_OPTION.RF0_OPTION_FPGA_STATE
+ cpld_regs.RF1_OPTION = cpld_regs.RF1_OPTION.RF1_OPTION_FPGA_STATE
+ cpld_regs.SW_RF0_CONFIG = 255
+ cpld_regs.SW_RF1_CONFIG = 255
+ cpld_regs.TX0_DSA1[0] = 0
+ cpld_regs.TX0_DSA2[0] = 0
+ cpld_regs.RX0_DSA1[0] = 0
+ cpld_regs.RX0_DSA2[0] = 0
+ cpld_regs.RX0_DSA3_A[0] = 0
+ cpld_regs.RX0_DSA3_B[0] = 0
+ cpld_regs.save_state()
+ # Now all the registers we touch will be enumerated by get_changed_addrs()
+ # Everything below *will* get written to the CPLD:
+ # ATR control
+ cpld_regs.RF0_OPTION = cpld_regs.RF0_OPTION.RF0_OPTION_SW_DEFINED
+ cpld_regs.RF1_OPTION = cpld_regs.RF1_OPTION.RF1_OPTION_SW_DEFINED
+ # Back to state 0 and sw-defined. That means nothing will get configured
+ # until UHD boots again.
+ cpld_regs.SW_RF0_CONFIG = 0
+ cpld_regs.SW_RF1_CONFIG = 0
+ # TX0 path control
+ cpld_regs.TX0_IF2_1_2[0] = cpld_regs.TX0_IF2_1_2[0].TX0_IF2_1_2_FILTER_2
+ cpld_regs.TX0_IF1_3[0] = cpld_regs.TX0_IF1_3[0].TX0_IF1_3_FILTER_0_3
+ cpld_regs.TX0_IF1_4[0] = cpld_regs.TX0_IF1_4[0].TX0_IF1_4_TERMINATION
+ cpld_regs.TX0_IF1_5[0] = cpld_regs.TX0_IF1_5[0].TX0_IF1_5_TERMINATION
+ cpld_regs.TX0_IF1_6[0] = cpld_regs.TX0_IF1_6[0].TX0_IF1_6_FILTER_0_3
+ cpld_regs.TX0_7[0] = cpld_regs.TX0_7[0].TX0_7_TERMINATION
+ cpld_regs.TX0_RF_8[0] = cpld_regs.TX0_RF_8[0].TX0_RF_8_RF_1
+ cpld_regs.TX0_RF_9[0] = cpld_regs.TX0_RF_9[0].TX0_RF_9_RF_1
+ cpld_regs.TX0_ANT_10[0] = cpld_regs.TX0_ANT_10[0].TX0_ANT_10_BYPASS_AMP
+ cpld_regs.TX0_ANT_11[0] = cpld_regs.TX0_ANT_11[0].TX0_ANT_11_BYPASS_AMP
+ cpld_regs.TX0_LO_13[0] = cpld_regs.TX0_LO_13[0].TX0_LO_13_INTERNAL
+ cpld_regs.TX0_LO_14[0] = cpld_regs.TX0_LO_14[0].TX0_LO_14_INTERNAL
+ # TX1 path control
+ cpld_regs.TX1_IF2_1_2[0] = cpld_regs.TX1_IF2_1_2[0].TX1_IF2_1_2_FILTER_2
+ cpld_regs.TX1_IF1_3[0] = cpld_regs.TX1_IF1_3[0].TX1_IF1_3_FILTER_0_3
+ cpld_regs.TX1_IF1_4[0] = cpld_regs.TX1_IF1_4[0].TX1_IF1_4_TERMINATION
+ cpld_regs.TX1_IF1_5[0] = cpld_regs.TX1_IF1_5[0].TX1_IF1_5_TERMINATION
+ cpld_regs.TX1_IF1_6[0] = cpld_regs.TX1_IF1_6[0].TX1_IF1_6_FILTER_0_3
+ cpld_regs.TX1_7[0] = cpld_regs.TX1_7[0].TX1_7_TERMINATION
+ cpld_regs.TX1_RF_8[0] = cpld_regs.TX1_RF_8[0].TX1_RF_8_RF_1
+ cpld_regs.TX1_RF_9[0] = cpld_regs.TX1_RF_9[0].TX1_RF_9_RF_1
+ cpld_regs.TX1_ANT_10[0] = cpld_regs.TX1_ANT_10[0].TX1_ANT_10_BYPASS_AMP
+ cpld_regs.TX1_ANT_11[0] = cpld_regs.TX1_ANT_11[0].TX1_ANT_11_BYPASS_AMP
+ cpld_regs.TX1_LO_13[0] = cpld_regs.TX1_LO_13[0].TX1_LO_13_INTERNAL
+ cpld_regs.TX1_LO_14[0] = cpld_regs.TX1_LO_14[0].TX1_LO_14_INTERNAL
+ # RX0 path control
+ cpld_regs.RX0_ANT_1[0] = cpld_regs.RX0_ANT_1[0].RX0_ANT_1_TERMINATION
+ cpld_regs.RX0_2[0] = cpld_regs.RX0_2[0].RX0_2_LOWBAND
+ cpld_regs.RX0_RF_3[0] = cpld_regs.RX0_RF_3[0].RX0_RF_3_RF_1
+ cpld_regs.RX0_4[0] = cpld_regs.RX0_4[0].RX0_4_LOWBAND
+ cpld_regs.RX0_IF1_5[0] = cpld_regs.RX0_IF1_5[0].RX0_IF1_5_FILTER_1
+ cpld_regs.RX0_IF1_6[0] = cpld_regs.RX0_IF1_6[0].RX0_IF1_6_FILTER_1
+ cpld_regs.RX0_LO_9[0] = cpld_regs.RX0_LO_9[0].RX0_LO_9_INTERNAL
+ cpld_regs.RX0_LO_10[0] = cpld_regs.RX0_LO_10[0].RX0_LO_10_INTERNAL
+ cpld_regs.RX0_RF_11[0] = cpld_regs.RX0_RF_11[0].RX0_RF_11_RF_3
+ # RX1 path control
+ cpld_regs.RX1_ANT_1[0] = cpld_regs.RX1_ANT_1[0].RX1_ANT_1_TERMINATION
+ cpld_regs.RX1_2[0] = cpld_regs.RX1_2[0].RX1_2_LOWBAND
+ cpld_regs.RX1_RF_3[0] = cpld_regs.RX1_RF_3[0].RX1_RF_3_RF_1
+ cpld_regs.RX1_4[0] = cpld_regs.RX1_4[0].RX1_4_LOWBAND
+ cpld_regs.RX1_IF1_5[0] = cpld_regs.RX1_IF1_5[0].RX1_IF1_5_FILTER_1
+ cpld_regs.RX1_IF1_6[0] = cpld_regs.RX1_IF1_6[0].RX1_IF1_6_FILTER_1
+ cpld_regs.RX1_LO_9[0] = cpld_regs.RX1_LO_9[0].RX1_LO_9_INTERNAL
+ cpld_regs.RX1_LO_10[0] = cpld_regs.RX1_LO_10[0].RX1_LO_10_INTERNAL
+ cpld_regs.RX1_RF_11[0] = cpld_regs.RX1_RF_11[0].RX1_RF_11_RF_3
+ # TX DSA
+ cpld_regs.TX0_DSA1[0] = 31
+ cpld_regs.TX0_DSA2[0] = 31
+ # RX DSA
+ cpld_regs.RX0_DSA1[0] = 15
+ cpld_regs.RX0_DSA2[0] = 15
+ cpld_regs.RX0_DSA3_A[0] = 15
+ cpld_regs.RX0_DSA3_B[0] = 15
+ for addr in cpld_regs.get_changed_addrs():
+ self.poke_cpld(addr, cpld_regs.get_reg(addr))
+ # pylint: enable=too-many-statements
+
+ #########################################################################
+ # UHD (De-)Initialization
+ #########################################################################
+ def init(self, args):
+ """
+ Execute necessary init dance to bring up dboard. This happens when a UHD
+ session starts.
+ """
+ self.log.debug("init() called with args `{}'".format(
+ ",".join(['{}={}'.format(x, args[x]) for x in args])
+ ))
+ return True
+
+ def deinit(self):
+ """
+ De-initialize after UHD session completes
+ """
+ self.log.debug("Setting CPLD back to safe defaults after UHD session.")
+ self._cpld_set_safe_defaults()
+
+ def tear_down(self):
+ self.db_iface.tear_down()
+
+ #########################################################################
+ # API calls needed by the zbx_dboard driver
+ #########################################################################
+ def enable_iq_swap(self, enable, trx, channel):
+ """
+ Turn on IQ swapping in the RFDC
+ """
+ self.db_iface.enable_iq_swap(enable, trx, channel)
+
+ def get_dboard_sample_rate(self):
+ """
+ Return the RFDC rate. This is usually a big number in the 3 GHz range.
+ """
+ return self.db_iface.get_sample_rate()
+
+ def get_dboard_prc_rate(self):
+ """
+ Return the PRC rate. The CPLD and LOs are clocked with this.
+ """
+ return self.db_iface.get_prc_rate()
+
+ def _has_compat_version(self, min_required_version):
+ """
+ Check for a minimum required version.
+ """
+ cpld_image_compat_revision = self.peek_cpld(self.regs.REVISION_addr)
+ return cpld_image_compat_revision >= min_required_version
+
+ def _get_cpld_git_hash(self):
+ """
+ Trace build of MB CPLD
+ """
+ git_hash_rb = self.peek_cpld(self.regs.GIT_HASH_addr)
+ (git_hash, dirtiness_qualifier) = parse_encoded_git_hash(git_hash_rb)
+ return "{:07x} ({})".format(git_hash, dirtiness_qualifier)
+
+ def reset_clock(self, value):
+ """
+ Disable PLL reference clock to enable SPLL reconfiguration
+
+ Puts the clock into reset if value is True, takes it out of reset
+ otherwise.
+ """
+ if self._clock_enabled != bool(value):
+ return
+ addr = self.regs.get_addr("PLL_REF_CLOCK_ENABLE")
+ enum = self.regs.PLL_REF_CLOCK_ENABLE_t
+ if value:
+ reg_value = enum.PLL_REF_CLOCK_ENABLE_DISABLE.value
+ else:
+ reg_value = enum.PLL_REF_CLOCK_ENABLE_ENABLE.value
+ self.poke_cpld(addr, reg_value)
+ self._clock_enabled = not bool(value)
+
+ #########################################################################
+ # LO SPI API
+ #
+ # We keep a LO peek/poke interface for debugging purposes.
+ #########################################################################
+ def _wait_for_spi_ready(self, timeout):
+ """ Returns False if a timeout occurred. timeout is in ms """
+ for _ in range(timeout):
+ if (self.peek_cpld(self._spi_addr) >> self.regs.SPI_READY_shift) \
+ & self.regs.SPI_READY_mask:
+ return True
+ time.sleep(0.001)
+ return False
+
+ def _lo_spi_send_tx(self, lo_name, write, addr, data=None):
+ """ Wait for SPI Ready and setup the TX data for a LO SPI transaction """
+ if not self._wait_for_spi_ready(timeout=100):
+ self.log.error('Timeout before LO SPI transaction waiting for SPI Ready')
+ raise RuntimeError('Timeout before LO SPI transaction waiting for SPI Ready')
+ lo_enum_name = 'LO_SELECT_' + lo_name.upper()
+ assert hasattr(self.regs.LO_SELECT_t, lo_enum_name), \
+ "Invalid LO name: {}".format(lo_name)
+ self.regs.LO_SELECT = getattr(self.regs.LO_SELECT_t, lo_enum_name)
+ if write:
+ self.regs.READ_FLAG = self.regs.READ_FLAG_t.READ_FLAG_WRITE
+ else:
+ self.regs.READ_FLAG = self.regs.READ_FLAG_t.READ_FLAG_READ
+ if data is not None:
+ self.regs.DATA = data
+ else:
+ self.regs.DATA = 0
+ self.regs.ADDRESS = addr
+ self.regs.START_TRANSACTION = \
+ self.regs.START_TRANSACTION_t.START_TRANSACTION_ENABLE
+ self.poke_cpld(self._spi_addr, self.regs.get_reg(self._spi_addr))
+
+ def _lo_spi_check_status(self, lo_name, addr, write=False):
+ """ Wait for SPI Ready and check the success of the LO SPI transaction """
+ # SPI Ready indicates that the previous transaction has completed
+ # and the RX data is ready to be consumed
+ if not write and not self._wait_for_spi_ready(timeout=100):
+ self.log.error('Timeout after LO SPI transaction waiting for SPI Ready')
+ raise RuntimeError('Timeout after LO SPI transaction waiting for SPI Ready')
+ # If the address or CS are not the same as what we set, there
+ # was interference during the SPI transaction
+ lo_select = self.regs.LO_SELECT.name[len('LO_SELECT_'):]
+ if self.regs.ADDRESS != addr or lo_select != lo_name.upper():
+ self.log.error('SPI transaction to LO failed!')
+ raise RuntimeError('SPI transaction to LO failed!')
+
+ def _lo_spi_get_rx(self):
+ """ Return RX data read from the LO SPI transaction """
+ spi_reg = self.peek_cpld(self._spi_addr)
+ return (spi_reg >> self.regs.DATA_shift) & self.regs.DATA_mask
+
+ def peek_lo_spi(self, lo_name, addr):
+ """ Perform a register read access to an LO via SPI """
+ self._lo_spi_send_tx(lo_name=lo_name, write=False, addr=addr)
+ self._lo_spi_check_status(lo_name, addr)
+ return self._lo_spi_get_rx()
+
+ def poke_lo_spi(self, lo_name, addr, val):
+ """ Perform a register write access to an LO via SPI """
+ self._lo_spi_send_tx(lo_name=lo_name, write=True, addr=addr, data=val)
+ self._lo_spi_check_status(lo_name, addr, write=True)
+
+ ###########################################################################
+ # LEDs
+ ###########################################################################
+ def set_leds(self, channel, rx, trx_rx, trx_tx):
+ """ Set the frontpanel LEDs """
+ assert channel in (0, 1)
+
+ self.regs.save_state()
+ if channel == 0:
+ # ensure to be in SW controlled mode
+ self.regs.RF0_OPTION = self.regs.RF0_OPTION.RF0_OPTION_SW_DEFINED
+ self.regs.SW_RF0_CONFIG = 0
+ self.regs.RX0_RX_LED[0] = self.regs.RX0_RX_LED[0].RX0_RX_LED_ENABLE \
+ if bool(rx) else self.regs.RX0_RX_LED[0].RX0_RX_LED_DISABLE
+ self.regs.RX0_TRX_LED[0] = self.regs.RX0_TRX_LED[0].RX0_TRX_LED_ENABLE \
+ if bool(trx_rx) else self.regs.RX0_TRX_LED[0].RX0_TRX_LED_DISABLE
+ self.regs.TX0_TRX_LED[0] = self.regs.TX0_TRX_LED[0].TX0_TRX_LED_ENABLE \
+ if bool(trx_tx) else self.regs.TX0_TRX_LED[0].TX0_TRX_LED_DISABLE
+ else:
+ # ensure to be in SW controlled mode
+ self.regs.RF1_OPTION = self.regs.RF1_OPTION.RF1_OPTION_SW_DEFINED
+ self.regs.SW_RF1_CONFIG = 0
+ self.regs.RX1_RX_LED[0] = self.regs.RX1_RX_LED[0].RX1_RX_LED_ENABLE \
+ if bool(rx) else self.regs.RX1_RX_LED[0].RX1_RX_LED_DISABLE
+ self.regs.RX1_TRX_LED[0] = self.regs.RX1_TRX_LED[0].RX1_TRX_LED_ENABLE \
+ if bool(trx_rx) else self.regs.RX1_TRX_LED[0].RX1_TRX_LED_DISABLE
+ self.regs.TX1_TRX_LED[0] = self.regs.TX1_TRX_LED[0].TX1_TRX_LED_ENABLE \
+ if bool(trx_tx) else self.regs.TX1_TRX_LED[0].TX1_TRX_LED_DISABLE
+
+ for addr in self.regs.get_changed_addrs():
+ self.poke_cpld(addr, self.regs.get_reg(addr))
+
+ ###########################################################################
+ # Sensors
+ ###########################################################################
+ def get_rf_temp_sensor(self, _):
+ """
+ Return the RF temperature sensor value
+ """
+ self.log.trace("Reading RF daughterboard temperature.")
+ sensor_names = [
+ f"TMP112 DB{self.slot_idx} Top",
+ f"TMP112 DB{self.slot_idx} Bottom",
+ ]
+ return get_temp_sensor(sensor_names, log=self.log)
diff --git a/mpm/python/usrp_mpm/dboard_manager/zbx_update_cpld.py b/mpm/python/usrp_mpm/dboard_manager/zbx_update_cpld.py
new file mode 100644
index 000000000..851cfe997
--- /dev/null
+++ b/mpm/python/usrp_mpm/dboard_manager/zbx_update_cpld.py
@@ -0,0 +1,218 @@
+#!/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 a ZBX daughterboard
+"""
+
+import sys
+import os
+import argparse
+import subprocess
+import pyudev
+from usrp_mpm.mpmlog import get_logger
+from usrp_mpm.mpmutils import check_fpga_state
+from usrp_mpm.sys_utils.sysfs_gpio import GPIOBank
+from usrp_mpm.periph_manager.x4xx_periphs import CtrlportRegs
+from usrp_mpm.periph_manager.x4xx_mb_cpld import MboardCPLD
+from usrp_mpm.chips.max10_cpld_flash_ctrl import Max10CpldFlashCtrl
+from usrp_mpm.sys_utils.udev import dt_symbol_get_spidev
+
+OPENOCD_DIR = "/usr/share/openocd/scripts"
+CONFIGS = {
+ 'axi_bitq' : {
+ 'files' : ["fpga/altera-10m50.cfg"],
+ 'cmd' : ["interface axi_bitq; axi_bitq_config %u %u %u; adapter_khz %u",
+ "init; svf -tap 10m50.tap %s -progress -quiet;exit"]
+ }
+}
+
+AXI_BITQ_ADAPTER_SPEED = 5000
+AXI_BITQ_BUS_CLK = 50000000
+
+#The offsets are for JTAG_DB0 and JTAG_DB1 on the motherboard CPLD
+DAUGHTERBOARD0_OFFSET = CtrlportRegs.MB_PL_CPLD + 0x60
+DAUGHTERBOARD1_OFFSET = CtrlportRegs.MB_PL_CPLD + 0x80
+
+# ZBX flash reconfiguration engine specific offsets
+RECONFIG_ENGINE_OFFSET = 0x20
+CPLD_MIN_REVISION = 0x20052016
+
+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 find_offset(dboard):
+ """
+ Find the AXI Bitq UIO device
+ :param dboard: the dboard, can be either 0 or 1
+ """
+ assert dboard in (0, 1)
+ return DAUGHTERBOARD0_OFFSET if dboard == 0 else DAUGHTERBOARD1_OFFSET
+
+def find_axi_bitq_uio():
+ """
+ Find the AXI Bitq UIO device
+ """
+ label = 'ctrlport-mboard-regs'
+
+ 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 do_update_cpld(filename, daughterboards, updater_mode):
+ """
+ Carry out update process for the CPLD
+ :param filename: path (on device) to the new CPLD image
+ :param daughterboards: iterable containing dboard numbers to update
+ :param updater_mode: the updater method to use- Either flash or legacy (JTAG)
+ :return: True on success, False otherwise
+ """
+ assert updater_mode in ('flash', 'legacy'), \
+ f"Invalid updater method {updater_mode} given"
+ logger = get_logger('update_cpld')
+ logger.info("Programming CPLD of dboards {} with image {} using {} mode"
+ .format(daughterboards, filename, updater_mode))
+
+ if not daughterboards:
+ logger.error("Invalid daughterboard selection.")
+ return False
+
+ if not os.path.exists(filename):
+ logger.error("CPLD image file {} not found".format(filename))
+ return False
+
+ if not check_fpga_state(logger=logger):
+ logger.error("CPLD lines are routed through fabric, FPGA is not programmed, giving up")
+ return False
+
+ if updater_mode == 'legacy':
+ return jtag_cpld_update(filename, daughterboards, logger)
+ # updater_mode == flash:
+ for dboard in daughterboards:
+ dboard = int(dboard, 10)
+ logger.info("Updating daughterboard slot {}...".format(dboard))
+ # enable required daughterboard clock
+ cpld_spi_node = dt_symbol_get_spidev('mb_cpld')
+ cpld_control = MboardCPLD(cpld_spi_node, logger)
+ cpld_control.enable_daughterboard_support_clock(dboard, enable=True)
+ # setup flash configuration engine and required register access
+ label = "ctrlport-mboard-regs"
+ ctrlport_regs = CtrlportRegs(label, logger)
+ regs = ctrlport_regs.get_db_cpld_iface(dboard)
+ flash_control = Max10CpldFlashCtrl(
+ logger, regs, RECONFIG_ENGINE_OFFSET, CPLD_MIN_REVISION)
+ success = flash_control.update(filename)
+ # disable clock
+ cpld_control.enable_daughterboard_support_clock(dboard, enable=False)
+ if not success:
+ return success
+ return True
+
+def jtag_cpld_update(filename, daughterboards, logger=None):
+ """
+ Carry out update process for the CPLD
+ :param filename: path (on device) to the new CPLD image
+ :param daughterboards: iterable containing dboard numbers to update
+ :return: True on success, False otherwise
+ """
+ mode = 'axi_bitq'
+ config = CONFIGS[mode]
+
+ if check_openocd_files(config['files'], logger=logger):
+ logger.trace("Found required OpenOCD files.")
+ else:
+ # check_openocd_files logs errors
+ return False
+
+ for dboard in daughterboards:
+ logger.info("Updating daughterboard slot {}...".format(dboard))
+
+ uio_id = find_axi_bitq_uio()
+ offset = find_offset(int(dboard, 10))
+ 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
+
+ cmd = [
+ "openocd",
+ "-c", config['cmd'][0] % (uio_id, AXI_BITQ_BUS_CLK, offset, 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)
+
+ logger.trace("Done programming CPLD...")
+ return True
+
+def main():
+ """
+ Go, go, go!
+ """
+ # Do some setup
+ def parse_args():
+ """Parse the command-line arguments"""
+ parser = argparse.ArgumentParser(description='Update the CPLD image on ZBX daughterboard')
+ parser.add_argument("--file", help="Filename of CPLD image",
+ default="/lib/firmware/ni/cpld-zbx.rpd")
+ parser.add_argument("--dboards", help="Slot name to program", default="0,1")
+ parser.add_argument("--updater",
+ help="The image updater method to use, either "
+ " 'legacy' (uses openocd) 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)
+
+ dboards = args.dboards.split(",")
+ if any([x not in ('0', '1') for x in dboards]):
+ log.error("Unsupported dboards requested: %s", dboards)
+ return False
+
+ return do_update_cpld(args.file, dboards, args.updater)
+
+
+if __name__ == "__main__":
+ sys.exit(not main())
diff --git a/mpm/python/usrp_mpm/mpmutils.py b/mpm/python/usrp_mpm/mpmutils.py
index 5da81ecfe..a569c85ad 100644
--- a/mpm/python/usrp_mpm/mpmutils.py
+++ b/mpm/python/usrp_mpm/mpmutils.py
@@ -8,6 +8,7 @@ Miscellaneous utilities for MPM
"""
import time
+import pyudev
from contextlib import contextmanager
def poll_with_timeout(state_check, timeout_ms, interval_ms):
@@ -90,18 +91,18 @@ def assert_compat_number(
log=None,
):
"""
- Check if a compat number pair is acceptable. A compat number is a pair of
- integers (MAJOR, MINOR). A compat number is not acceptable if the major
+ Check if a compat number tuple is acceptable. A compat number is a tuple of
+ integers (MAJOR, MINOR, BUILD). A compat number is not acceptable if the major
part differs from the expected value (regardless of how it's different) or
if the minor part is behind the expected value and fail_on_old_minor was
- given.
+ given. Build number is not checked here.
On failure, will throw a RuntimeError.
Arguments:
- expected_compat -- A tuple (major, minor) which represents the compat
- number we are expecting.
- actual_compat -- A tuple (major, minor) which represents the compat number
- that is actually available.
+ expected_compat -- A tuple (major, minor) or (major, minor, build) which
+ represents the compat number we are expecting.
+ actual_compat -- A tuple (major, minor) or (major, minor, build) which
+ represents the compat number that is actually available.
component -- A name of the component for which we are checking the compat
number, e.g. "FPGA".
fail_on_old_minor -- Will also fail if the actual minor compat number is
@@ -110,15 +111,20 @@ def assert_compat_number(
log -- Logger object. If given, will use this to report on intermediate
steps and non-fatal minor compat mismatches.
"""
- assert len(expected_compat) == 2
- assert len(actual_compat) == 2
+ valid_tuple_lengths = (2, 3)
+ assert len(expected_compat) in valid_tuple_lengths, (
+ f"Version {expected_compat} has invalid format. Valid formats are"
+ "(major, minor) or (major, minor, build)")
+ assert len(actual_compat) in valid_tuple_lengths, (
+ f"Version {expected_compat} has invalid format. Valid formats are"
+ "(major, minor) or (major, minor, build)")
log_err = lambda msg: log.error(msg) if log is not None else None
log_warn = lambda msg: log.warning(msg) if log is not None else None
expected_actual_str = "Expected: {:d}.{:d} Actual: {:d}.{:d}".format(
expected_compat[0], expected_compat[1],
actual_compat[0], actual_compat[1],
)
- component_str = "" if component is None else " for component `{}'".format(
+ component_str = "" if component is None else " for component '{}'".format(
component
)
if actual_compat[0] != expected_compat[0]:
@@ -128,7 +134,7 @@ def assert_compat_number(
log_err(err_msg)
raise RuntimeError(err_msg)
if actual_compat[1] > expected_compat[1]:
- log_warn("Actual minor compat ahead of expected compat{}. {}".format(
+ log_warn("Minor compat ahead of expected compat{}. {}".format(
component_str, expected_actual_str
))
if actual_compat[1] < expected_compat[1]:
@@ -139,7 +145,6 @@ def assert_compat_number(
log_err(err_msg)
raise RuntimeError(err_msg)
log_warn(err_msg)
- return
def str2bool(value):
"""Return a Boolean value from a string, even if the string is not simply
@@ -188,3 +193,21 @@ def lock_guard(lockable):
finally:
lockable.unlock()
+def check_fpga_state(which=0, logger=None):
+ """
+ Check if the FPGA is operational
+ :param which: the FPGA to check
+ """
+ try:
+ context = pyudev.Context()
+ fpga_mgrs = list(context.list_devices(subsystem="fpga_manager"))
+ if fpga_mgrs:
+ state = fpga_mgrs[which].attributes.asstring('state')
+ if logger is not None:
+ logger.trace("FPGA State: {}".format(state))
+ return state == "operating"
+ return False
+ except OSError as ex:
+ if logger is not None:
+ logger.error("Error while checking FPGA status: {}".format(ex))
+ return False
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())
diff --git a/mpm/python/usrp_mpm/sys_utils/CMakeLists.txt b/mpm/python/usrp_mpm/sys_utils/CMakeLists.txt
index 71a06faf8..f3f3f40d4 100644
--- a/mpm/python/usrp_mpm/sys_utils/CMakeLists.txt
+++ b/mpm/python/usrp_mpm/sys_utils/CMakeLists.txt
@@ -15,6 +15,7 @@ set(USRP_MPM_SYSUTILS_FILES
${CMAKE_CURRENT_SOURCE_DIR}/udev.py
${CMAKE_CURRENT_SOURCE_DIR}/uio.py
${CMAKE_CURRENT_SOURCE_DIR}/watchdog.py
+ ${CMAKE_CURRENT_SOURCE_DIR}/ectool.py
)
list(APPEND USRP_MPM_FILES ${USRP_MPM_SYSUTILS_FILES})
set(USRP_MPM_FILES ${USRP_MPM_FILES} PARENT_SCOPE)
diff --git a/mpm/python/usrp_mpm/sys_utils/ectool.py b/mpm/python/usrp_mpm/sys_utils/ectool.py
new file mode 100644
index 000000000..0525d604a
--- /dev/null
+++ b/mpm/python/usrp_mpm/sys_utils/ectool.py
@@ -0,0 +1,45 @@
+#
+# Copyright 2020 Ettus Research, a National Instruments Brand
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+"""
+Utilities for interfacing with ectool
+"""
+
+import subprocess
+
+def run_cmd(cmd):
+ """Run ectool utility's command named cmd."""
+ cmd_str = ' '.join(['ectool', cmd])
+ try:
+ output = subprocess.check_output(
+ cmd_str,
+ stderr=subprocess.STDOUT,
+ shell=True,
+ )
+ except subprocess.CalledProcessError as ex:
+ raise RuntimeError("Failed to execute {} ectool command".format(cmd))
+ return output.decode("utf-8")
+
+def get_num_fans():
+ """ Run ectool utility's command pwmgetnumfans to get number of fans."""
+ output = run_cmd('pwmgetnumfans')
+ num_fans = [int(s) for s in output.split() if s.isdigit()][0]
+ return num_fans
+
+def get_fan_rpm():
+ """Run ectool utility's command pwmgetfanrpm to get fan rpm."""
+ num_fans = get_num_fans()
+ if num_fans == 0:
+ raise RuntimeError("Number of fans is zero.")
+ output = run_cmd('pwmgetfanrpm')
+ fan_rpm_info = [int(s) for s in output.split() if s.isdigit()]
+ if len(fan_rpm_info) == 2 * num_fans:
+ return {
+ "fan{}".format(fan) : fan_rpm_info[fan * 2 + 1]
+ for fan in range (0, num_fans)
+ }
+ else:
+ raise RuntimeError("Error getting fan rpm using ectool, at least one fan" \
+ " may be stalled. Command output: {}".format(output))
diff --git a/mpm/python/usrp_mpm/sys_utils/gpio.py b/mpm/python/usrp_mpm/sys_utils/gpio.py
index b609479f1..e864149f3 100644
--- a/mpm/python/usrp_mpm/sys_utils/gpio.py
+++ b/mpm/python/usrp_mpm/sys_utils/gpio.py
@@ -30,6 +30,7 @@ class Gpio:
INPUT = gpiod.LINE_REQ_DIR_IN
OUTPUT = gpiod.LINE_REQ_DIR_OUT
+ FALLING_EDGE = gpiod.LINE_REQ_EV_FALLING_EDGE
def __init__(self, name, direction=INPUT, default_val=None):
self._direction = direction
@@ -58,3 +59,12 @@ class Gpio:
with request_gpio(self._line, self._direction) as gpio:
gpio.set_value(int(value))
self._out_value = bool(value)
+
+ def event_wait(self):
+ """
+ Wait for an event to happen on this line
+ """
+ with request_gpio(self._line, self._direction) as gpio:
+ while True:
+ if gpio.event_wait(sec=1):
+ return True
diff --git a/mpm/python/usrp_mpm/sys_utils/sysfs_gpio.py b/mpm/python/usrp_mpm/sys_utils/sysfs_gpio.py
index b542123bc..056c28fd0 100644
--- a/mpm/python/usrp_mpm/sys_utils/sysfs_gpio.py
+++ b/mpm/python/usrp_mpm/sys_utils/sysfs_gpio.py
@@ -145,6 +145,7 @@ class SysFSGPIO(object):
self._use_mask = use_mask
self._ddr = ddr
self._init_value = init_value
+ self._out_value = 0
self.log.trace("Generating SysFSGPIO object for identifiers `{}'..."
.format(identifiers))
self._gpio_dev, self._map_info = \
@@ -182,6 +183,8 @@ class SysFSGPIO(object):
open(os.path.join(GPIO_SYSFS_BASE_DIR, 'export'), 'w').write('{}'.format(gpio_num))
ddr_str = 'out' if ddr_out else 'in'
ddr_str = 'high' if ini_v else ddr_str
+ if ini_v and ddr_out:
+ self._out_value |= 1 << gpio_idx
self.log.trace("On GPIO path `{}', setting DDR mode to {}.".format(gpio_path, ddr_str))
open(os.path.join(GPIO_SYSFS_BASE_DIR, gpio_path, 'direction'), 'w').write(ddr_str)
@@ -196,12 +199,18 @@ class SysFSGPIO(object):
value = 1
assert (1<<gpio_idx) & self._use_mask
assert (1<<gpio_idx) & self._ddr
+ assert int(value) in [0, 1]
+ value = int(value)
gpio_num = self._base_gpio + gpio_idx
gpio_path = os.path.join(GPIO_SYSFS_BASE_DIR, 'gpio{}'.format(gpio_num))
value_path = os.path.join(gpio_path, GPIO_SYSFS_VALUEFILE)
self.log.trace("Writing value `{}' to `{}'...".format(value, value_path))
assert os.path.exists(value_path)
open(value_path, 'w').write('{}'.format(value))
+ if value:
+ self._out_value |= 1 << gpio_idx
+ else:
+ self._out_value &= ~(1 << gpio_idx)
def reset(self, gpio_idx):
"""
@@ -216,19 +225,23 @@ class SysFSGPIO(object):
"""
Read back a GPIO at given index.
- Note: The GPIO must be in the valid range, and it's DDR value must be
- low (for "in").
+ Note: The GPIO must be in the valid range. If it's DDR value is
+ low (for "in") then the register value is read from a local variable.
"""
assert (1<<gpio_idx) & self._use_mask
- assert (1<<gpio_idx) & (~self._ddr)
- gpio_num = self._base_gpio + gpio_idx
- gpio_path = os.path.join(GPIO_SYSFS_BASE_DIR, 'gpio{}'.format(gpio_num))
- value_path = os.path.join(gpio_path, GPIO_SYSFS_VALUEFILE)
- assert os.path.exists(value_path)
- read_value = int(open(value_path, 'r').read().strip())
- self.log.trace("Reading value {} from `{}'...".format(read_value, value_path))
+ if (1<<gpio_idx) & (~self._ddr):
+ gpio_num = self._base_gpio + gpio_idx
+ gpio_path = os.path.join(GPIO_SYSFS_BASE_DIR, 'gpio{}'.format(gpio_num))
+ value_path = os.path.join(gpio_path, GPIO_SYSFS_VALUEFILE)
+ assert os.path.exists(value_path)
+ read_value = int(open(value_path, 'r').read().strip())
+ self.log.trace("Reading value {} from `{}'...".format(read_value, value_path))
+ else:
+ read_value = 1 if self._out_value & (1 << gpio_idx) else 0
+ self.log.trace("Reading value {} from local var".format(read_value))
return read_value
+
class GPIOBank:
"""
Usability / convenience wrapper for GPIO banks accessed by SysFSGPIO
diff --git a/mpm/python/usrp_mpm/sys_utils/uio.py b/mpm/python/usrp_mpm/sys_utils/uio.py
index 84e4b2b64..f660c39b3 100644
--- a/mpm/python/usrp_mpm/sys_utils/uio.py
+++ b/mpm/python/usrp_mpm/sys_utils/uio.py
@@ -144,15 +144,15 @@ class UIO(object):
else:
self.log.trace("Using UIO device by label `{0}'".format(label))
self._path, map_info = find_uio_device(label, self.log)
+ if self._path is None or map_info is None:
+ self.log.error("Could not find a UIO device for label {0}".format(label))
+ raise RuntimeError("Could not find a UIO device for label {0}".format(label))
# TODO If we ever support multiple maps, check if this is correct...
offset = offset or map_info['offset']
assert offset == 0 # ...and then remove this line
length = length or map_info['size']
self.log.trace("UIO device is being opened read-{0}.".format(
"only" if read_only else "write"))
- if self._path is None:
- self.log.error("Could not find a UIO device for label {0}".format(label))
- raise RuntimeError("Could not find a UIO device for label {0}".format(label))
self._read_only = read_only
# Our UIO objects are managed in C++ land, which gives us more granular control over
# opening and closing
diff --git a/mpm/python/x4xx_bist b/mpm/python/x4xx_bist
new file mode 100644
index 000000000..adda5ecb0
--- /dev/null
+++ b/mpm/python/x4xx_bist
@@ -0,0 +1,1048 @@
+#!/usr/bin/env python3
+#
+# Copyright 2020 Ettus Research, a National Instruments Brand
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+"""
+X4XX Built-In Self Test (BIST)
+
+Will work on all derivatives of the X4xx series.
+"""
+
+import sys
+import time
+import subprocess
+from usrp_mpm.sys_utils.udev import dt_symbol_get_spidev
+from usrp_mpm import bist
+
+# Timeout values are in seconds:
+GPS_WARMUP_TIMEOUT = 70 # Data sheet says "about a minute"
+GPS_LOCKOK_TIMEOUT = 2 # Data sheet says about 15 minutes. Because our test
+ # does not necessarily require GPS lock to pass, we
+ # reduce this value in order for the BIST to pass faster
+ # by default.
+DEFAULT_DB_ID = 0
+
+# We will import stuff as late as possible, intentionally, so let's calm down
+# PyLint
+# pylint: disable=import-outside-toplevel
+
+
+def get_rfdc_config(log):
+ """
+ Return resampling, halfband settings of current FPGA image.
+ """
+ from usrp_mpm.periph_manager import x4xx_rfdc_regs
+ from usrp_mpm.periph_manager import x4xx_rfdc_ctrl
+ rdfc_regs_control = x4xx_rfdc_regs.RfdcRegsControl(
+ x4xx_rfdc_ctrl.X4xxRfdcCtrl.rfdc_regs_label, log)
+ return rdfc_regs_control.get_rfdc_resampling_factor(0)
+
+
+##############################################################################
+# Bist class
+##############################################################################
+class X4XXBIST(bist.UsrpBIST):
+ """
+ BIST Tool for the USRP X4xx series
+ """
+ usrp_type = "X4XX"
+ # This defines special tests that are really collections of other tests.
+ collections = {
+ 'standard': ["gpsdo", "rtc", "temp", "fan"],
+ 'extended': "*",
+ }
+ # Default FPGA image type
+ DEFAULT_FPGA_TYPE = 'X4_200'
+ lv_compat_format = {
+ 'ddr3': {
+ 'throughput': -1,
+ },
+ 'gpsdo': {
+ "class": "",
+ "time": "",
+ "ept": -1,
+ "lat": -1,
+ "lon": -1,
+ "alt": -1,
+ "epx": -1,
+ "epy": -1,
+ "epv": -1,
+ "track": -1,
+ "speed": -1,
+ "climb": -1,
+ "eps": -1,
+ "mode": -1,
+ },
+ 'gpio': {
+ 'write_patterns': [],
+ 'read_patterns': [],
+ },
+ 'temp': {
+ "DRAM PCB": 40000,
+ "EC Internal": 41000,
+ "PMBUS-0": 42000,
+ "PMBUS-1": 43000,
+ "Power Supply PCB": 44000,
+ "RFSoC": 45000,
+ "Sample Clock PCB": 46000,
+ "TMP464 Internal": 47000,
+ },
+ 'fan': {
+ 'fan0': -1,
+ 'fan1': -1,
+ },
+ }
+ device_args = "type=x4xx,mgmt_addr=127.0.0.1"
+
+ def __init__(self):
+ bist.UsrpBIST.__init__(self)
+
+ def get_mb_periph_mgr(self):
+ """Return reference to an x4xx periph manager"""
+ from usrp_mpm.periph_manager.x4xx import x4xx
+ return x4xx
+
+ def get_product_id(self):
+ """Return the mboard product ID:"""
+ # TODO: use correct product ID, this is just a hack
+ # TODO: the path to the eeprom is not self-speaking like e.g. mb_eeprom
+ # return bist.get_product_id_from_eeprom(valid_ids=['x410'], eeprom='mb_eeprom')
+ return self.get_product_id_from_eeprom(
+ valid_ids=['0410'],
+ eeprom='/sys/bus/nvmem/devices/13-00500/nvmem')
+
+ def get_product_id_from_eeprom(self, valid_ids, eeprom):
+ """Return the mboard product ID
+ Returns something like x410...
+ """
+ # it is just here as override because eeprom-id installed on the
+ # x410 now needs a parameter while on n310 it runs without
+ # also the path to the eeprom is not self-speaking like e.g. mb_eeprom
+ # last problem is that the returned id is not 'x410' but '0410'
+ cmd = ['eeprom-id']
+ cmd.append(eeprom)
+ cmd = ' '.join(cmd)
+ output = subprocess.check_output(
+ cmd,
+ stderr=subprocess.STDOUT,
+ shell=True,
+ ).decode('utf-8')
+ sys.stderr.write("product_id_from_eeprom: {}".format(str(output)))
+ for valid_id in valid_ids:
+ if valid_id in output:
+ return 'x410'
+ raise AssertionError("Cannot determine product ID.: `{}'".format(output))
+
+ def reload_fpga(self, fpga_type, sfp0_addrs):
+ """
+ Loads the specified fpga and checks for sfp0 address, if the sfp0 address
+ existed, restores the original sfp0 address
+ """
+ from usrp_mpm.sys_utils import net
+ from pyroute2 import IPRoute
+ import errno
+ bist.load_fpga_image(
+ fpga_type,
+ self.device_args,
+ 'x410',
+ )
+ if sfp0_addrs:
+ ipr = IPRoute()
+ dev = ipr.link_lookup(ifname='sfp0')[0]
+ ipr.link('set', index=dev, state='down')
+ try:
+ ipr.addr('add', index=dev, address=sfp0_addrs[0], mask=24, label='sfp0')
+ except Exception as ex:
+ # If the addr already exists then ignore the error
+ if ex.code == errno.EEXIST:
+ pass
+ ipr.link('set', index=dev, state='up')
+
+ def check_fpga_type_clkaux(self):
+ """
+ clkaux BISTs require a OSS bitfile, check type and reimage if needed
+ If we have an OSS bitfile, we need to return a mcr that the clkaux
+ lmk can lock to. (Data clock rate of 122.88MHz)
+ """
+ from usrp_mpm.periph_manager import x4xx, x4xx_periphs
+
+ mboard_regs_control = x4xx_periphs.MboardRegsControl(
+ x4xx.x4xx.mboard_regs_label, self.log)
+ fpga_str = mboard_regs_control.get_fpga_type()
+ if not fpga_str or fpga_str == 'LV':
+ bist.load_fpga_image(
+ self.DEFAULT_FPGA_TYPE,
+ self.device_args,
+ 'x410',
+ )
+
+ rfdc_resamp, halfband = get_rfdc_config(self.log)
+ mcr = 122880000 * (8 / rfdc_resamp)
+ if halfband:
+ mcr = mcr / 2
+ return mcr
+
+#############################################################################
+# BISTS
+# All bist_* methods must return True/False success values!
+#############################################################################
+ def bist_gpsdo(self):
+ """
+ BIST for GPSDO
+ Description: Returns GPS information
+ External Equipment: None; Recommend attaching an antenna or providing
+ fake GPS information
+
+ Return dictionary: A TPV dictionary as returned by gpsd.
+ See also: http://www.catb.org/gpsd/gpsd_json.html
+
+ Check for mode 2 or 3 to see if it's locked.
+ """
+ assert 'gpsdo' in self.tests_to_run
+ if self.args.dry_run:
+ return True, {
+ "class": "TPV",
+ "time": "2017-04-30T11:48:20.10Z",
+ "ept": 0.005,
+ "lat": 30.407899,
+ "lon": -97.726634,
+ "alt": 1327.689,
+ "epx": 15.319,
+ "epy": 17.054,
+ "epv": 124.484,
+ "track": 10.3797,
+ "speed": 0.091,
+ "climb": -0.085,
+ "eps": 34.11,
+ "mode": 3
+ }
+ from usrp_mpm.periph_manager import x4xx
+ # Turn on GPS, give some time to acclimatize
+ clk_aux_brd = x4xx.ClockingAuxBrdControl(default_source="gpsdo")
+ time.sleep(5)
+ gps_warmup_timeout = float(
+ self.args.option.get('gps_warmup_timeout', GPS_WARMUP_TIMEOUT))
+ gps_lockok_timeout = float(
+ self.args.option.get('gps_lockok_timeout', GPS_LOCKOK_TIMEOUT))
+ # Wait for WARMUP to go low
+ sys.stderr.write(
+ "Waiting for WARMUP to go low for up to {} seconds...\n".format(
+ gps_warmup_timeout))
+ if not bist.poll_with_timeout(
+ lambda: not clk_aux_brd.get_gps_warmup(),
+ gps_warmup_timeout*1000, 1000
+ ):
+ raise RuntimeError(
+ "GPS-WARMUP did not go low within {} seconds!".format(
+ gps_warmup_timeout))
+ sys.stderr.write("Chip is warmed up.\n")
+ # Wait for LOCKOK. Data sheet says wait up to 15 minutes for GPS lock.
+ sys.stderr.write(
+ "Waiting for LOCKOK to go high for up to {} seconds...\n".format(
+ gps_lockok_timeout))
+ if not bist.poll_with_timeout(
+ clk_aux_brd.get_gps_lock,
+ gps_lockok_timeout*1000,
+ 1000
+ ):
+ sys.stderr.write("No GPS-LOCKOK!\n")
+ sys.stderr.write("GPS-SURVEY status: {}\n".format(
+ clk_aux_brd.get_gps_survey()
+ ))
+ sys.stderr.write("GPS-PHASELOCK status: {}\n".format(
+ clk_aux_brd.get_gps_phase_lock()
+ ))
+ sys.stderr.write("GPS-ALARM status: {}\n".format(
+ clk_aux_brd.get_gps_alarm()
+ ))
+ # Now the chip is on, read back the TPV result
+ result = bist.get_gpsd_tpv_result()
+ # If we reach this line, we have a valid result and the chip responded.
+ # However, it doesn't necessarily mean we had a GPS lock.
+ return True, result
+
+ def bist_ref_clock_mboard(self):
+ """
+ BIST for clock lock from mboard clock source (local to the motherboard)
+
+ Description: Checks to see if the motherboard can lock to its internal
+ clock source.
+
+ External Equipment: None
+ Return dictionary:
+ - <sensor-name>:
+ - locked: Boolean lock status
+
+ There can be multiple ref lock sensors; for a pass condition they all
+ need to be asserted.
+ """
+ assert 'ref_clock_mboard' in self.tests_to_run
+ if self.args.dry_run:
+ return True, {'ref_locked': True}
+ from usrp_mpm.periph_manager import x4xx_rfdc_ctrl
+ rfdc_resamp, fpga_halfband = get_rfdc_config(self.log)
+ mcrs_to_test = []
+ for master_clock_rate, (_, decimation, _, halfband) in \
+ x4xx_rfdc_ctrl.X4xxRfdcCtrl.master_to_sample_clk.items():
+ if decimation == rfdc_resamp and fpga_halfband == halfband:
+ mcrs_to_test.append(master_clock_rate)
+
+ for master_clock_rate in mcrs_to_test:
+ sys.stderr.write("Testing master_clock_rate {}".format(master_clock_rate))
+ result = bist.get_ref_clock_prop(
+ 'mboard',
+ 'internal',
+ extra_args={
+ 'addr': '169.254.0.2',
+ 'mgmt_addr': '127.0.0.1',
+ 'master_clock_rate': master_clock_rate,
+ }
+ )
+ if 'error_msg' in result:
+ return False, result
+ return True, result
+
+ def bist_ref_clock_ext(self):
+ """
+ BIST for clock lock from external source.
+
+ Description: Checks to see if the motherboard can lock to the external
+ reference clock.
+
+ External Equipment: 10 MHz reference source connected to "ref in".
+
+ Return dictionary:
+ - <sensor-name>:
+ - locked: Boolean lock status
+
+ There can be multiple ref lock sensors; for a pass condition they all
+ need to be asserted.
+ """
+ assert 'ref_clock_ext' in self.tests_to_run
+ if self.args.dry_run:
+ return True, {'ref_locked': True}
+ result = bist.get_ref_clock_prop(
+ 'external',
+ 'external',
+ extra_args={'addr': '169.254.0.2', 'mgmt_addr': '127.0.0.1'}
+ )
+ return 'error_msg' not in result, result
+
+ def bist_ref_clock_gpsdo(self):
+ """
+ BIST for clock lock from gpsdo source.
+
+ Description: Checks to see if the motherboard can lock to the gpsdo
+ reference clock.
+
+ External Equipment: None
+
+ Return dictionary:
+ - <sensor-name>:
+ - locked: Boolean lock status
+
+ There can be multiple ref lock sensors; for a pass condition they all
+ need to be asserted.
+ """
+ assert 'ref_clock_gpsdo' in self.tests_to_run
+ if self.args.dry_run:
+ return True, {'ref_locked': True}
+ result = bist.get_ref_clock_prop(
+ 'gpsdo',
+ 'gpsdo',
+ extra_args={}
+ )
+ return 'error_msg' not in result, result
+
+ def bist_ref_clock_int(self):
+ """
+ BIST for clock lock from internal source.
+
+ Description: Checks to see if the motherboard can lock to the
+ clocking aux board's internal reference clock.
+
+ External Equipment: None
+
+ Return dictionary:
+ - <sensor-name>:
+ - locked: Boolean lock status
+
+ There can be multiple ref lock sensors; for a pass condition they all
+ need to be asserted.
+ """
+ assert 'ref_clock_int' in self.tests_to_run
+ if self.args.dry_run:
+ return True, {'ref_locked': True}
+ result = bist.get_ref_clock_prop(
+ 'internal',
+ 'internal',
+ extra_args={'addr': '169.254.0.2', 'mgmt_addr': '127.0.0.1'}
+ )
+ return 'error_msg' not in result, result
+
+ def bist_ref_clock_nsync(self):
+ """
+ BIST for clock lock from nsync source.
+
+ Description: Checks to see if the motherboard can lock to the nsync
+ reference clock.
+
+ External Equipment: None
+
+ Return dictionary:
+ - <sensor-name>:
+ - locked: Boolean lock status
+
+ There can be multiple ref lock sensors; for a pass condition they all
+ need to be asserted.
+ """
+ assert 'ref_clock_nsync' in self.tests_to_run
+ if self.args.dry_run:
+ return True, {'ref_locked': True}
+ result = bist.get_ref_clock_prop(
+ 'nsync',
+ 'internal',
+ extra_args={'addr': '169.254.0.2', 'mgmt_addr': '127.0.0.1'}
+ )
+ return 'error_msg' not in result, result
+
+ def bist_nsync_fabric(self):
+ """
+ BIST for testing the fabric_clk signal from the motherboard
+
+ Description: Checks to see if the LMK on the clocking auxiliary board
+ can lock to the fabric clock signal output by the motherboard. We check
+ this by verifying that the pri_ref signal is being used, the dpll is locked,
+ the apll1 is locked, and apll2 is unlocked.
+
+ External Equipment: None
+
+ Return dictionary:
+ - fabric_clk pll lock: Did the clkaux lmk lock to the fabric_clk signal
+ """
+ assert 'nsync_fabric' in self.tests_to_run
+ import uhd
+ from uhd.usrp import multi_usrp
+
+ try:
+ mcr = self.check_fpga_type_clkaux()
+ usrp_dev = multi_usrp.MultiUSRP("type=x4xx,addr=localhost,clock_source=nsync,"
+ "master_clock_rate={}".format(str(mcr)))
+ except Exception as ex:
+ return False, {
+ 'error_msg': "Failed to create usrp device: {}".format(str(ex))
+ }
+
+ mpm_c = usrp_dev.get_mpm_client()
+
+ mpm_c.nsync_change_input_source('fabric_clk')
+ # The lock should happen fast, but give it half a second
+ time.sleep(0.5)
+ # Status 1 checks that the lmk is configured to use the priref signal
+ # True = secref, False = priref
+ using_pri_ref = not mpm_c.clkaux_get_nsync_status1()
+ # Status 0 checks dpll loss of lock
+ dpll_lock = not mpm_c.clkaux_get_nsync_status0()
+ # Reg 0xD checks several APLL statuses, we need to make sure APLL1 is
+ # locked and APLL2 is unlocked
+ apll_lock = mpm_c.peek_clkaux(0xD) == '0x8'
+
+ result = using_pri_ref and dpll_lock and apll_lock
+
+ mpm_c.enable_ecpri_clocks(False)
+
+ return result, {"pri_ref_selected": using_pri_ref,
+ "dpll_locked": dpll_lock,
+ "apll1_locked_apll2_unlocked": apll_lock}
+
+ def bist_nsync_gty(self):
+ """
+ BIST for testing the gty_rcv_clk signal from the motherboard
+
+ Description: Checks to see if the LMK on the clocking auxiliary board
+ can lock to the gty_rcv clock signal output by the motherboard. We check
+ this by verifying that the pri_ref signal is being used, the dpll is locked,
+ the apll1 is locked, and apll2 is unlocked.
+
+ External Equipment: None
+
+ Return dictionary:
+ - gty_rcv_clk pll lock: Did the clkaux lmk lock to the gty_rcv_clk signal
+ """
+ assert 'nsync_gty' in self.tests_to_run
+ import uhd
+ from uhd.usrp import multi_usrp
+
+ try:
+ mcr = self.check_fpga_type_clkaux()
+ usrp_dev = multi_usrp.MultiUSRP("type=x4xx,addr=localhost,clock_source=nsync,"
+ "master_clock_rate={}".format(str(mcr)))
+ except Exception as ex:
+ return False, {
+ 'error_msg': "Failed to create usrp device: {}".format(str(ex))
+ }
+
+ mpm_c = usrp_dev.get_mpm_client()
+
+ mpm_c.nsync_change_input_source('gty_rcv_clk')
+ # The lock should happen fast, but give it half a second
+ time.sleep(0.5)
+ # Status 1 checks that the lmk is configured to use the priref signal
+ # True = secref, False = priref
+ using_pri_ref = not mpm_c.clkaux_get_nsync_status1()
+ # Status 0 checks dpll loss of lock
+ dpll_lock = not mpm_c.clkaux_get_nsync_status0()
+ # Reg 0xD checks several APLL statuses, we need to make sure APLL1 is
+ # locked and APLL2 is unlocked
+ apll_lock = mpm_c.peek_clkaux(0xD) == '0x8'
+
+ result = using_pri_ref and dpll_lock and apll_lock
+
+ mpm_c.enable_ecpri_clocks(False)
+ # Set the clock source back to internal, as the register values
+ # for locking to the gty_rcv_clk cause the mboard to lose ref_lock
+ # to the nsync lmk.
+ mpm_c.set_clock_source('internal')
+
+ return result, {"pri_ref_selected": using_pri_ref,
+ "dpll_locked": dpll_lock,
+ "apll1_locked_apll2_unlocked": apll_lock}
+
+ def bist_clkaux_fpga_aux_ref(self):
+ """
+ BIST for testing the fpga_aux_ref pps source
+
+ Description: Checks to see if the fpga_aux_ref can output a valid pps signal
+
+ External Equipment: None
+
+ Return dictionary:
+ """
+ assert 'clkaux_fpga_aux_ref' in self.tests_to_run
+ import uhd
+ from uhd.usrp import multi_usrp
+
+ try:
+ mcr = self.check_fpga_type_clkaux()
+ usrp_dev = multi_usrp.MultiUSRP("type=x4xx,addr=localhost,clock_source=nsync,"
+ "master_clock_rate={}".format(str(mcr)))
+ except Exception as ex:
+ return False, {
+ 'error_msg': "Failed to create usrp device: {}".format(str(ex))
+ }
+
+ mpm_c = usrp_dev.get_mpm_client()
+
+ count = mpm_c.get_fpga_aux_ref_freq()
+
+ # We expect a pps signal, pulse is reported in 40 MHz clock ticks, so
+ # 1 PPS is expected to return 40 million ticks, this gives 1% tolerance
+ return 39600000 < count < 40400000, {}
+
+ def bist_nsync_rpll_config(self):
+ """
+ BIST for testing that the LMK28PRIRefClk can be used as a source for the
+ motherboard RPLL
+
+ Description: Enable the LMK on the clocking auxiliary board to output a signal
+ on the LMK28PRIRefClk line to the motherboard RPLL. Configure the RPLL to use
+ this source, and check to see if the RPLL can lock to the source.
+
+ External Equipment: None
+
+ Return dictionary:
+ """
+ assert 'nsync_rpll_config' in self.tests_to_run
+ import uhd
+ from uhd.usrp import multi_usrp
+ from usrp_mpm.sys_utils import net
+
+ try:
+ mcr = self.check_fpga_type_clkaux()
+ usrp_dev = multi_usrp.MultiUSRP("type=x4xx,addr=localhost,clock_source=nsync,"
+ "master_clock_rate={}".format(str(mcr)))
+ except Exception as ex:
+ return False, {
+ 'error_msg': "Failed to create usrp device: {}".format(str(ex))
+ }
+
+ sfp0_addrs = net.get_iface_info('sfp0')['ip_addrs']
+
+ mpm_c = usrp_dev.get_mpm_client()
+
+ mpm_c.config_rpll_to_nsync()
+
+ ref_locked = mpm_c.get_ref_lock_sensor()
+
+ result = ref_locked.get('value') == 'true'
+
+ fpga_type = mpm_c.get_device_info()['fpga']
+
+ del usrp_dev
+
+ # This is a brute-force way of making sure the device gets back into a clean state after
+ # we touched the rpll configuration
+ self.reload_fpga(fpga_type, sfp0_addrs)
+
+ return result, ref_locked
+
+ def bist_gpio(self):
+ """
+ BIST for GPIO
+ Description: Writes and reads the values to the GPIO
+
+ Needed Equipment: External loopback cable between port 0 and port 1
+
+ Notes:
+ - X410 has two FP-GPIO connectors (HDMI connectors) with 12 programmable
+ pins each
+
+ Return dictionary:
+ - write_patterns: A list of patterns that were written
+ - read_patterns: A list of patterns that were read back
+ """
+ assert 'gpio' in self.tests_to_run
+ if self.args.dry_run:
+ patterns = range(64)
+ return True, {
+ 'write_patterns': list(patterns),
+ 'read_patterns': list(patterns),
+ }
+ from usrp_mpm.periph_manager import x4xx, x4xx_periphs, x4xx_mb_cpld
+ mboard_regs_control = x4xx_periphs.MboardRegsControl(
+ x4xx.x4xx.mboard_regs_label, self.log)
+ cpld_spi_node = dt_symbol_get_spidev('mb_cpld')
+ cpld_control = x4xx_mb_cpld.MboardCPLD(cpld_spi_node, self.log)
+ gpio_diocontrol = x4xx_periphs.DioControl(
+ mboard_regs_control,
+ cpld_control,
+ self.log)
+ def _run_sub_test(inport, outport, pin_mode, voltage, pattern):
+ """
+ Closure to run an actual test. The GPIO control object is enclosed.
+
+ Arguments:
+ inport: "port" argument for DioControl, input port
+ outport: "port" argument for DioControl, input port
+ pin_mode: HDMI or DIO (see DioControl)
+ voltage: Valid arg for DioControl.set_voltage_level()
+ pattern: Bits to write to the inport, should be read back at outport
+ """
+ gpio_diocontrol.set_port_mapping(pin_mode)
+ # We set all pins to be driven by the PS
+ # in HDMI mode not all pins can be accessed by the user
+ if pin_mode == "HDMI":
+ mask = 0xDB6D
+ else:
+ mask = 0xFFF
+ gpio_diocontrol.set_pin_masters(inport, mask)
+ gpio_diocontrol.set_pin_masters(outport, mask)
+ gpio_diocontrol.set_voltage_level(inport, voltage)
+ gpio_diocontrol.set_voltage_level(outport, voltage)
+ gpio_diocontrol.set_pin_directions(inport, 0x00000)
+ gpio_diocontrol.set_pin_directions(outport, 0xFFFFF)
+ gpio_diocontrol.set_pin_outputs(outport, pattern)
+ read_values = gpio_diocontrol.get_pin_inputs(inport)
+ if (pattern & mask) != read_values:
+ sys.stderr.write(gpio_diocontrol.status())
+ return False, {'write_patterns': ["0x{:04X}".format(pattern)],
+ 'read_patterns': ["0x{:04X}".format(read_values)]}
+ return True, {'write_patterns': ["0x{:04X}".format(pattern)],
+ 'read_patterns': ["0x{:04X}".format(read_values)]}
+ # Now run tests:
+ for voltage in ["1V8", "2V5", "3V3"]:
+ for mode in ["DIO", "HDMI"]:
+ for pattern in [0xFFFF, 0xA5A5, 0x5A5A, 0x0000]:
+ sys.stderr.write("test: PortA -> PortB, {}, {}, 0x{:04X}"
+ .format(voltage, mode, pattern))
+ status, data = _run_sub_test(
+ "PORTB", "PORTA", mode, voltage, pattern)
+ if not status:
+ return status, data
+ sys.stderr.write("test: PortB -> PortA, {}, {}, 0x{:04X}"
+ .format(voltage, mode, pattern))
+ status, data = _run_sub_test(
+ "PORTA", "PORTB", mode, voltage, pattern)
+ if not status:
+ return status, data
+ return status, data
+
+
+ def bist_qsfp(self):
+ """
+ BiST for QSFP status and property read out.
+
+ Description: Tests MODSEL (write) and MODPRS (read) pin at QSFP port
+ and I2C communication with QSFP module. By default all
+ ports are tested. The user can add module to the option
+ argument when calling the test to select a specific
+ port out of [0,1]. A negative number will test all
+ ports (the default)
+
+ Example: The following example will run the test on port 0:
+ > x4xx_bist qsfp --option module=0
+
+ Needed Equipment: None, for exhaustive test results the test should
+ be run with loopback test modules.
+
+ Notes: The test ensures consistency of I2C communication and the
+ MODSEL and MODPRS pins. If MODPRS pin is active low
+ (module present) I2C communication must return valid values for
+ all QSFP properties. On the other hand when MODPRS pin is high
+ QSFP properties should report None as the only valid value.
+ The test also disables the I2C communication (unsetting MODSEL
+ pin) and checks that the low level I2C communication with the
+ module fails. The communication is reenabled after 3sec. This
+ window allows a tester to check whether a loopback adapter
+ signals the state of MODSEL correctly.
+ """
+
+ assert 'qsfp' in self.tests_to_run
+ if self.args.dry_run:
+ return True, {}
+
+ from usrp_mpm.periph_manager import x4xx
+ from usrp_mpm.periph_manager.x4xx_periphs import QSFPModule
+
+ def add_error_msg(msg):
+ # add error message to result map
+ if "error_msg" not in infos:
+ infos["error_msg"] = []
+ infos["error_msg"].append(msg)
+
+ def modules_to_test():
+ module = self.args.option.get("module", -1)
+ try:
+ module = int(module)
+ except ValueError:
+ add_error_msg("Module '{}' is not an int".format(module))
+ return None
+ if module < 0:
+ return x4xx.X400_QSFP_I2C_CONFIGS
+ if module not in [0, 1]:
+ add_error_msg("Module to test must be 0 or 1")
+ return None
+ return [x4xx.X400_QSFP_I2C_CONFIGS[module]]
+
+
+ infos = {}
+ modules_to_test = modules_to_test()
+ if not modules_to_test:
+ return False, infos
+
+ result = True
+
+ for config in modules_to_test:
+ qsfp = QSFPModule(
+ config.modprs, config.modsel, config.devsymbol, self.log)
+ info = {}
+ info["available"] = qsfp.is_available()
+ if info["available"]:
+ # if adapter is available, read out prop via I2C and
+ # verify they are all valid (not None)
+ props = [qsfp.decoded_status,
+ qsfp.vendor_name,
+ qsfp.connector_type]
+ for prop in props:
+ info[prop.__name__] = prop()
+ if not all(info.values()):
+ result = False
+ add_error_msg("QSFP adapter at {} is present but has "
+ "None property.".format(config.devsymbol))
+ else:
+ # if adapter is not available
+ # reading a property *must* return None
+ if qsfp.connector_type() is not None:
+ result = False
+ add_error_msg("QSFP adapter at {} reports valid property "
+ "but is not present.".format(config.devsymbol))
+
+ infos[config.devsymbol] = info
+
+ # disable i2c communication and check for failure on low level read
+ qsfp.enable_i2c(False)
+ try:
+ qsfp.qsfp_regs.peek8(0)
+ result = False
+ add_error_msg("Reading I2C when disabled should fail with "
+ "RuntimeError at {}\n".format(config.devsymbol))
+ except RuntimeError:
+ pass
+ sys.stderr.write("A loopback QSFP adapter at {} should report MODSEL "
+ "inactive for 3 sec.".format(config.devsymbol))
+ time.sleep(3)
+ qsfp.enable_i2c(True)
+
+ return result, infos
+
+
+ def bist_temp(self):
+ """
+ BIST for temperature sensors
+ Description: Reads the temperature sensors on the motherboards and
+ returns their values in mC
+
+ Return dictionary:
+ - <thermal-zone-name>: temp in mC
+ """
+ assert 'temp' in self.tests_to_run
+ if self.args.dry_run:
+ return True, {"DRAM PCB": 40000,
+ "EC Internal": 41000,
+ "PMBUS-0": 42000,
+ "PMBUS-1": 43000,
+ "Power Supply PCB": 44000,
+ "RFSoC": 45000,
+ "Sample Clock PCB": 46000,
+ "TMP464 Internal": 47000,
+ }
+
+ result = bist.get_iio_temp_sensor_values()
+
+ if len(result) < 1:
+ result['error_msg'] = "No temperature sensors found!"
+
+ return 'error_msg' not in result, result
+
+ def bist_fan(self):
+ """
+ BIST for fans
+ Description: Reads the RPM values of the fans on the motherboard
+
+ Return dictionary:
+ - <fan-name>: Fan speed in RPM
+
+ External Equipment: None
+ """
+ assert 'fan' in self.tests_to_run
+ if self.args.dry_run:
+ return True, {'fan0': 10000, 'fan1': 10000}
+ result = bist.get_ectool_fan_values()
+ return len(result) == 2, result
+
+ def _db_flash_init(self, db_flash):
+ """
+ Initialize the specified DB Flash and verify
+ its state
+ """
+ db_flash.init()
+ if not db_flash.initialized:
+ raise RuntimeError()
+
+ def _db_flash_deinit(self, db_flash):
+ """
+ De-initialize the specified DB Flash and verify
+ its state
+ """
+ db_flash.deinit()
+ if db_flash.initialized:
+ raise RuntimeError()
+
+ def bist_spi_flash_integrity(self):
+ """
+ BIST for SPI flash on DB
+ Description: Performs data integrity test on a section of
+ the flash memory. Stop the MPM service before running this
+ test using "systemctl stop usrp-hwd" command.
+
+ External Equipment: None, but at least one daughterboard
+ should be installed in the X410 unit.
+
+ Return dictionary:
+
+ """
+ assert 'spi_flash_integrity' in self.tests_to_run
+ if self.args.dry_run:
+ return True, {}
+ import os
+ from usrp_mpm.sys_utils.db_flash import DBFlash
+ FIXED_MEMORY_PATTERN = 'fixed'
+ RANDOM_MEMORY_PATTERN = 'random'
+
+ db_id = int(self.args.option.get('db_id', DEFAULT_DB_ID))
+ assert db_id in [0, 1]
+
+ db_flash = DBFlash(db_id, log=None)
+
+ buf_size = 100
+ memory_pattern = str(self.args.option.get('memory_pattern', RANDOM_MEMORY_PATTERN))
+ assert memory_pattern in (FIXED_MEMORY_PATTERN, RANDOM_MEMORY_PATTERN)
+ if memory_pattern == RANDOM_MEMORY_PATTERN:
+ buff = os.urandom(buf_size)
+ else:
+ buff = [0xA5] * buf_size
+
+ data_valid = False
+
+ try:
+ self._db_flash_init(db_flash)
+ except (ValueError, RuntimeError) as ex:
+ return False, {"error_msg": "Error while initializing flash storage: " + str(ex)}
+
+ file_path = f'/mnt/db{db_id}_flash/test.bin'
+
+ sys.stderr.write("Testing DB{} with {} memory pattern..".format(db_id, memory_pattern))
+ with open(file_path, "wb") as f:
+ f.write(bytearray(buff))
+
+ try:
+ self._db_flash_deinit(db_flash)
+ self._db_flash_init(db_flash)
+ except (ValueError, RuntimeError) as ex:
+ return False, {"error_msg": "Error while init(deinit)ializing flash storage: " + str(ex)}
+
+ with open(file_path, "rb") as f:
+ read_data = f.read()
+ data_valid = read_data == bytearray(buff)
+ os.remove(file_path)
+ self._db_flash_deinit(db_flash)
+
+ return data_valid, {}
+
+ def bist_spi_flash_speed(self):
+ """
+ BIST for SPI flash on DB
+ Description: Performs read and write speed test on the SPI flash
+ memory on DB. Stop the MPM service before running this test
+ using "systemctl stop usrp-hwd" command.
+
+ External Equipment: None, but at least one daughterboard
+ should be installed in the X410 unit.
+
+ Return dictionary:
+
+ """
+ assert 'spi_flash_speed' in self.tests_to_run
+ if self.args.dry_run:
+ return True, {}
+ import os
+ import re
+ from usrp_mpm.sys_utils.db_flash import DBFlash
+
+ MIN_WRITE_SPEED = 5000 #B/s
+ MIN_READ_SPEED = 5000 #B/s
+ def parse_speed(cmd_output):
+ mobj = re.search(
+ r"(.*records in\n)(.*records out\n)(.* (?P<speed>[0-9.]+) (?P<order>\S?)B\/s)",
+ cmd_output)
+ if mobj is None:
+ return 0
+ scale = {'': 1, 'k': 1024, 'M': 1024*1024, 'G': 1024*1024*1024}
+ order = mobj.group('order')
+ if order not in scale:
+ raise ValueError(f"unsupported unit '{order}B/s'")
+ return float(mobj.group('speed')) * scale[order]
+
+ db_id = int(self.args.option.get('db_id', DEFAULT_DB_ID))
+ assert db_id in [0, 1]
+
+ db_flash = DBFlash(db_id, log=None)
+
+ file_path = f'/mnt/db{db_id}_flash/test.bin'
+
+ try:
+ self._db_flash_init(db_flash)
+ except (ValueError, RuntimeError) as ex:
+ return False, {
+ "error_msg": "Error while initializing flash storage: {}".format(str(ex))
+ }
+
+ sys.stderr.write("Testing DB{}..".format(db_id))
+ write_error_msg = None
+ sys.stderr.write("Write Speed Test:")
+ cmd = [
+ 'dd',
+ 'if=/dev/zero',
+ 'of=' + file_path,
+ 'bs=512',
+ 'count=1000',
+ 'oflag=dsync'
+ ]
+
+ try:
+ output = subprocess.check_output(
+ cmd,
+ stderr=subprocess.STDOUT,
+ )
+ except subprocess.CalledProcessError as ex:
+ output = ex.output
+ write_error_msg = "Error during Write Test: {}".format(output)
+ write_test_output = output.decode("utf-8")
+ sys.stderr.write(write_test_output)
+
+ # De-init and init flash here to mitigate any effects
+ # caching may have on the read speed.
+ try:
+ self._db_flash_deinit(db_flash)
+ self._db_flash_init(db_flash)
+ except (ValueError, RuntimeError) as ex:
+ return False, {
+ "error_msg": "Error while init(deinit)ializing flash storage: {}".format(str(ex))}
+
+ sys.stderr.write("Read Speed Test:")
+ read_error_msg = None
+ cmd = [
+ 'dd',
+ 'if=' + file_path,
+ 'of=/dev/null',
+ 'bs=512',
+ 'count=1000',
+ 'oflag=dsync'
+ ]
+
+ try:
+ output = subprocess.check_output(
+ cmd,
+ stderr=subprocess.STDOUT,
+ )
+ except subprocess.CalledProcessError as ex:
+ output = ex.output
+ read_error_msg = "Error during Read Test: {}".format(output)
+ read_test_output = output.decode("utf-8")
+ sys.stderr.write(read_test_output)
+
+ # Clean up.
+ os.remove(file_path)
+ self._db_flash_deinit(db_flash)
+
+ test_status = bool(write_error_msg is None and read_error_msg is None)
+
+ if test_status:
+ write_speed = parse_speed(write_test_output)
+ read_speed = parse_speed(read_test_output)
+
+ if write_speed == 0:
+ test_status = False
+ write_error_msg = "Write speed parse error"
+ elif write_speed < MIN_WRITE_SPEED:
+ test_status = False
+ write_error_msg = \
+ "Write speed {} B/s is below minimum requirement of {} B/s".format(
+ write_speed, MIN_WRITE_SPEED)
+
+ if read_speed == 0:
+ test_status = False
+ read_error_msg = "Read speed parse error"
+ elif read_speed < MIN_READ_SPEED:
+ test_status = False
+ read_error_msg = \
+ "Read speed {} B/s is below minimum requirement of {} B/s".format(
+ read_speed, MIN_READ_SPEED)
+
+ return test_status, {
+ "Write Test": write_test_output if write_error_msg is None else write_error_msg,
+ "Read Test": read_test_output if read_error_msg is None else read_error_msg
+ }
+
+
+##############################################################################
+# main
+##############################################################################
+def main():
+ " Go, go, go! "
+ return X4XXBIST().run()
+
+if __name__ == '__main__':
+ sys.exit(not main())