diff options
Diffstat (limited to 'mpm/python/usrp_mpm/dboard_manager')
-rw-r--r-- | mpm/python/usrp_mpm/dboard_manager/CMakeLists.txt | 5 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/dboard_manager/__init__.py | 5 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/dboard_manager/base.py | 37 | ||||
-rw-r--r--[-rwxr-xr-x] | mpm/python/usrp_mpm/dboard_manager/dboard_iface.py | 68 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/dboard_manager/empty_slot.py | 37 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/dboard_manager/x4xx_db_iface.py | 144 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/dboard_manager/x4xx_debug_db.py | 152 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/dboard_manager/x4xx_if_test_cca.py | 167 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/dboard_manager/zbx.py | 461 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/dboard_manager/zbx_update_cpld.py | 218 |
10 files changed, 1271 insertions, 23 deletions
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()) |