aboutsummaryrefslogtreecommitdiffstats
path: root/mpm/python
diff options
context:
space:
mode:
Diffstat (limited to 'mpm/python')
-rw-r--r--mpm/python/usrp_mpm/dboard_manager/CMakeLists.txt8
-rw-r--r--mpm/python/usrp_mpm/dboard_manager/__init__.py1
-rw-r--r--mpm/python/usrp_mpm/dboard_manager/adc_rh.py242
-rw-r--r--mpm/python/usrp_mpm/dboard_manager/dac_rh.py342
-rw-r--r--mpm/python/usrp_mpm/dboard_manager/gain_rh.py93
-rw-r--r--mpm/python/usrp_mpm/dboard_manager/gaintables_rh.py271
-rw-r--r--mpm/python/usrp_mpm/dboard_manager/lmk_rh.py316
-rw-r--r--mpm/python/usrp_mpm/dboard_manager/rh_init.py481
-rw-r--r--mpm/python/usrp_mpm/dboard_manager/rh_periphs.py193
-rw-r--r--mpm/python/usrp_mpm/dboard_manager/rhodium.py573
-rw-r--r--mpm/python/usrp_mpm/periph_manager/n3xx.py6
11 files changed, 2525 insertions, 1 deletions
diff --git a/mpm/python/usrp_mpm/dboard_manager/CMakeLists.txt b/mpm/python/usrp_mpm/dboard_manager/CMakeLists.txt
index 64217be07..0b34e4d70 100644
--- a/mpm/python/usrp_mpm/dboard_manager/CMakeLists.txt
+++ b/mpm/python/usrp_mpm/dboard_manager/CMakeLists.txt
@@ -11,8 +11,16 @@ SET(USRP_MPM_FILES ${USRP_MPM_FILES})
SET(USRP_MPM_DBMGR_FILES
${CMAKE_CURRENT_SOURCE_DIR}/__init__.py
${CMAKE_CURRENT_SOURCE_DIR}/base.py
+ ${CMAKE_CURRENT_SOURCE_DIR}/rhodium.py
+ ${CMAKE_CURRENT_SOURCE_DIR}/rh_init.py
+ ${CMAKE_CURRENT_SOURCE_DIR}/rh_periphs.py
+ ${CMAKE_CURRENT_SOURCE_DIR}/lmk_rh.py
+ ${CMAKE_CURRENT_SOURCE_DIR}/adc_rh.py
+ ${CMAKE_CURRENT_SOURCE_DIR}/dac_rh.py
${CMAKE_CURRENT_SOURCE_DIR}/eiscat.py
${CMAKE_CURRENT_SOURCE_DIR}/neon.py
+ ${CMAKE_CURRENT_SOURCE_DIR}/gain_rh.py
+ ${CMAKE_CURRENT_SOURCE_DIR}/gaintables_rh.py
${CMAKE_CURRENT_SOURCE_DIR}/lmk_eiscat.py
${CMAKE_CURRENT_SOURCE_DIR}/lmk_mg.py
${CMAKE_CURRENT_SOURCE_DIR}/magnesium.py
diff --git a/mpm/python/usrp_mpm/dboard_manager/__init__.py b/mpm/python/usrp_mpm/dboard_manager/__init__.py
index 8f6e5da96..70e7881db 100644
--- a/mpm/python/usrp_mpm/dboard_manager/__init__.py
+++ b/mpm/python/usrp_mpm/dboard_manager/__init__.py
@@ -8,6 +8,7 @@ dboards module __init__.py
"""
from .base import DboardManagerBase
from .magnesium import Magnesium
+from .rhodium import Rhodium
from .neon import Neon
from .eiscat import EISCAT
from .test import test
diff --git a/mpm/python/usrp_mpm/dboard_manager/adc_rh.py b/mpm/python/usrp_mpm/dboard_manager/adc_rh.py
new file mode 100644
index 000000000..2befa011f
--- /dev/null
+++ b/mpm/python/usrp_mpm/dboard_manager/adc_rh.py
@@ -0,0 +1,242 @@
+#
+# Copyright 2018 Ettus Research, a National Instruments Company
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+"""
+AD9695 driver for use with Rhodium
+"""
+
+import time
+from builtins import object
+from ..mpmlog import get_logger
+
+class AD9695Rh(object):
+ """
+ This class provides an interface to configure the AD9695 IC through SPI.
+ """
+
+ ADC_CHIP_ID = 0xDE
+
+ CHIP_CONFIGURATION_REG = 0x0002
+ CHIP_ID_LSB_REG = 0x0004
+ SCRATCH_PAD_REG = 0x000A
+
+ def __init__(self, slot_idx, regs_iface, parent_log=None):
+ self.log = parent_log.getChild("AD9695") if parent_log is not None \
+ else get_logger("AD9695-{}".format(slot_idx))
+ self.regs = regs_iface
+ assert hasattr(self.regs, 'peek8')
+ assert hasattr(self.regs, 'poke8')
+
+ def _verify_chip_id():
+ chip_id = self.regs.peek8(self.CHIP_ID_LSB_REG)
+ self.log.trace("ADC Chip ID: 0x{:X}".format(chip_id))
+ if chip_id != self.ADC_CHIP_ID:
+ self.log.error("Wrong Chip ID 0x{:X}".format(chip_id))
+ return False
+ return True
+
+ if not _verify_chip_id():
+ raise RuntimeError("Unable to locate AD9695")
+
+
+ def assert_scratch(self, scratch_val=0xAD):
+ """
+ Method that validates the scratch register by poking and peeking.
+ """
+ self.regs.poke8(self.SCRATCH_PAD_REG, scratch_val)
+ self.log.trace("Scratch write value: 0x{:X}".format(scratch_val))
+ scratch_rb = self.regs.peek8(self.SCRATCH_PAD_REG) & 0xFF
+ self.log.trace("Scratch readback: 0x{:X}".format(scratch_rb))
+ if scratch_rb != scratch_val:
+ raise RuntimeError("Wrong ADC scratch readback: 0x{:X}".format(scratch_rb))
+
+ def pokes8(self, addr_vals):
+ """
+ Apply a series of pokes.
+ pokes8((0,1),(0,2)) is the same as calling poke8(0,1), poke8(0,2).
+ """
+ for addr, val in addr_vals:
+ self.regs.poke8(addr, val)
+
+
+ def power_down_channel(self, power_down=False):
+ """
+ This method either powers up/down the channel according to register 0x0002 [1:0]:
+ power_down = True -> Power-down mode.
+ Digital datapath clocks disabled; digital datapath held
+ in reset; JESD204B interface disabled.
+ power_down = False -> Normal mode.
+ Channel powered up.
+ """
+ power_mode = 0b11 if power_down else 0b00
+ self.regs.poke8(self.CHIP_CONFIGURATION_REG, power_mode)
+
+
+ def init(self):
+ """
+ Basic init that resets the ADC and verifies it.
+ """
+ self.power_down_channel(False) # Power-up the channel.
+ self.log.trace("Reset ADC & Verify")
+ self.regs.poke8(0x0000, 0x81) # Soft-reset the ADC (self-clearing).
+ time.sleep(0.005) # We must allow 5 ms for the ADC's bootloader.
+ self.assert_scratch(0xAD) # Verify scratch register R/W access.
+ self.regs.poke8(0x0571, 0x15) # Powerdown the JESD204B serial transmit link.
+ self.log.trace("ADC's JESD204B link powered down.")
+
+
+ def config(self):
+ """
+ Check the clock status, and write configuration values!
+ Before performing the above, the chip is soft-reset through the
+ serial interface.
+ """
+ self.init()
+
+ clock_status = self.regs.peek8(0x011B) & 0xFF
+ self.log.trace("Clock status readback: 0x{:X}".format(clock_status))
+ if clock_status != 0x01:
+ self.log.error("Input clock not detected")
+ raise RuntimeError("Input clock not detected for ADC")
+
+ self.log.trace("ADC Configuration.")
+ self.pokes8((
+ (0x003F, 0x80), # Disable PDWN/STBY pin.
+ (0x0040, 0x00), # FD_A|B pins configured as Fast Detect outputs.
+ (0x0559, 0x11), # Configure tail bits as overrange bits (1:0) (Based on VST2).
+ (0x055A, 0x01), # Configure tail bits as overrange bits (2) (Based on VST2).
+ (0x058D, 0x17), # Set K = d23 (0x17) (Frames per multiframe).
+ (0x0550, 0x00), # Test mode pattern generation: OFF.
+ (0x0571, 0x15), # JESD Link mode: ILA enabled with K28.3 and K28.7; link powered down.
+ (0x058F, 0x0D), # CS = 0 (Control bits); N = 14 (ADC resolution).
+ (0x056E, 0x10), # JESD204B lane rate range: 3.375 Gbps to 6.75 Gbps.
+ (0x058B, 0x03), # Scrambling disabled; L = 4 (number of lanes).
+ (0x058C, 0x00), # F = 1 (0x0 + 1) (Number of octets per frame)
+ (0x058E, 0x01), # M = 2 (0x1) (Number of converters per link).
+ (0x05B2, 0x01), # SERDOUT0 mapped to lane 1.
+ (0x05B3, 0x00), # SERDOUT1 mapped to lane 0.
+ (0x05C0, 0x10), # SERDOUT0 voltage swing adjust (improves RX margin at FPGA).
+ (0x05C1, 0x10), # SERDOUT1 voltage swing adjust (improves RX margin at FPGA).
+ (0x05C2, 0x10), # SERDOUT2 voltage swing adjust (improves RX margin at FPGA).
+ (0x05C3, 0x10), # SERDOUT3 voltage swing adjust (improves RX margin at FPGA).
+ ))
+ self.log.trace("ADC register dump finished.")
+
+
+ def init_framer(self):
+ """
+ Initialize the ADC's framer, and check the PLL for lock.
+ """
+ def _check_pll_lock():
+ pll_lock_status = self.regs.peek8(0x056F)
+ if (pll_lock_status & 0x88) != 0x80:
+ self.log.debug("PLL reporting unlocked... Status: 0x{:x}"
+ .format(pll_lock_status))
+ return False
+ return True
+
+ self.log.trace("Initializing framer...")
+ self.pokes8((
+ (0x0571, 0x14), # Powerup the link before JESD204B initialization.
+ (0x1228, 0x4F), # Reset JESD204B start-up circuit
+ (0x1228, 0x0F), # JESD204B start-up circuit in normal operation. The value may be 0x00.
+ (0x1222, 0x00), # JESD204B PLL force normal operation
+ (0x1222, 0x04), # Reset JESD204B PLL calibration
+ (0x1222, 0x00), # JESD204B PLL normal operation
+ (0x1262, 0x08), # Clear loss of lock bit
+ (0x1262, 0x00), # Loss of lock bit normal operation
+ ))
+
+ self.log.trace("Polling for PLL lock...")
+ locked = False
+ for _ in range(6):
+ time.sleep(0.001)
+ # Clear stickies possibly?
+ if _check_pll_lock():
+ locked = True
+ self.log.info("ADC PLL Locked!")
+ break
+ if not locked:
+ raise RuntimeError("ADC PLL did not lock! Check the logs for details.")
+
+ self.log.trace("ADC framer initialized.")
+
+ def enable_sysref_capture(self, enabled=False):
+ """
+ Enable the SYSREF capture block.
+ """
+ sysref_ctl1 = 0x00 # Default value is disabled.
+ if enabled:
+ sysref_ctl1 = 0b1 << 2 # N-Shot SYSREF mode
+
+ self.log.trace("%s ADC SYSREF capture..." % {True: 'Enabling', False: 'Disabling'}[enabled])
+ self.pokes8((
+ (0x0120, sysref_ctl1), # Capture low-to-high N-shot SYSREF transitions on CLK's RE
+ (0x0121, 0x00), # Capture the next SYSREF only.
+ ))
+ self.log.trace("ADC SYSREF capture %s." % {True: 'enabled', False: 'disabled'}[enabled])
+
+
+ def check_framer_status(self):
+ """
+ This function checks the status of the framer by checking SYSREF capture regs.
+ """
+ SYSREF_MONITOR_MESSAGES = {
+ 0 : "Condition not defined!",
+ 1 : "Possible setup error. The smaller the setup, the smaller its margin.",
+ 2 : "No setup or hold error (best hold margin).",
+ 3 : "No setup or hold error (best setup and hold margin).",
+ 4 : "No setup or hold error (best setup margin).",
+ 5 : "Possible hold error. The largest the hold, the smaller its margin.",
+ 6 : "Possible setup or hold error."
+ }
+
+ # This is based of Table 37 in the AD9695's datasheet.
+ def _decode_setup_hold(setup, hold):
+ status = 0
+ if setup == 0x0:
+ if hold == 0x0:
+ status = 6
+ elif hold == 0x8:
+ status = 4
+ elif (hold >= 0x9) and (hold <= 0xF):
+ status = 5
+ elif (setup <= 0x7) and (hold == 0x0):
+ status = 1
+ elif (setup == 0x8) and ((hold >= 0x0) and (hold <= 0x8)):
+ status = 2
+ elif hold == 0x8:
+ status = 3
+ return status
+
+ self.log.trace("Checking ADC's framer status.")
+
+ # Read the SYSREF setup and hold monitor register.
+ sysref_setup_hold_monitor = self.regs.peek8(0x0128)
+ sysref_setup = sysref_setup_hold_monitor & 0x0F
+ sysref_hold = (sysref_setup_hold_monitor & 0xF0) >> 4
+ sysref_monitor_status = _decode_setup_hold(sysref_setup, sysref_hold)
+ self.log.trace("SYSREF setup: 0x{:X}".format(sysref_setup))
+ self.log.trace("SYSREF hold: 0x{:X}".format(sysref_hold))
+ self.log.debug("SYSREF monitor: %s" % SYSREF_MONITOR_MESSAGES[sysref_monitor_status])
+
+ # Read the Clock divider phase when SYSREF is captured.
+ sysref_phase = self.regs.peek8(0x0129) & 0x0F
+ self.log.trace("SYSREF capture was %.2f cycle(s) delayed from clock.", (sysref_phase * 0.5))
+ self.log.trace("Clk divider phase when SYSREF was captured: 0x{:X}".format(sysref_phase))
+
+ # Read the SYSREF counter.
+ sysref_count = self.regs.peek8(0x012A) & 0xFF
+ self.log.trace("%d SYSREF events were captured." % sysref_count)
+
+ if sysref_count == 0x0:
+ self.log.error("A SYSREF event was not captured by the ADC.")
+ elif sysref_monitor_status == 0:
+ self.log.trace("The SYSREF setup & hold monitor status is not defined.")
+ elif sysref_monitor_status in (1, 5, 6):
+ self.log.warning("SYSREF monitor: %s" % SYSREF_MONITOR_MESSAGES[sysref_monitor_status])
+ elif sysref_phase > 0x0:
+ self.log.trace("SYSREF capture was %.2f cycle(s) delayed from clock." % (sysref_phase * 0.5))
+ return sysref_count >= 0x01
diff --git a/mpm/python/usrp_mpm/dboard_manager/dac_rh.py b/mpm/python/usrp_mpm/dboard_manager/dac_rh.py
new file mode 100644
index 000000000..7fd32e8de
--- /dev/null
+++ b/mpm/python/usrp_mpm/dboard_manager/dac_rh.py
@@ -0,0 +1,342 @@
+#
+# Copyright 2018 Ettus Research, a National Instruments Company
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+"""
+DAC37J82 driver for use with Rhodium
+"""
+
+import time
+from builtins import object
+from ..mpmlog import get_logger
+
+class DAC37J82Rh(object):
+ """
+ This class provides an interface to configure the DAC37J82 IC through SPI.
+ """
+
+ DAC_VENDOR_ID = 0b01
+ DAC_VERSION_ID = 0b010 # Version used in Rhodium Rev. A
+
+ def __init__(self, slot_idx, regs_iface, parent_log=None):
+ self.log = parent_log.getChild("DAC37J82") if parent_log is not None \
+ else get_logger("DAC37J82-{}".format(slot_idx))
+ self.slot_idx = slot_idx
+ self.regs = regs_iface
+ assert hasattr(self.regs, 'peek16')
+ assert hasattr(self.regs, 'poke16')
+
+ def _verify_chip_id():
+ chip_id = self.regs.peek16(0x7F) & 0x001F
+ self.log.trace("DAC Vendor & Version ID: 0x{:X}".format(chip_id))
+ if chip_id != ((self.DAC_VENDOR_ID << 3) | self.DAC_VERSION_ID):
+ self.log.error("Wrong Vendor & Version 0x{:X}".format(chip_id))
+ return False
+ return True
+
+ self.reset()
+
+ if not _verify_chip_id():
+ raise RuntimeError("Unable to locate DAC37J82")
+
+ # Define variable configuration per slot.
+ # The JESD lanes going to the DAC pins are swapped differently:
+ # DBA: 0 -> 0 / 1 -> 1 / 2 -> 2 / 3 -> 3
+ # DBB: 0 -> 0 / 1 -> 1 / 2 -> 3 / 3 -> 2
+ # Therefore, depending on the DB that is being configured, we need
+ # to change the JESD lanes internal routing in the DAC to compensate
+ # for the board traces swapping.
+ self.lanes_ids_1 = {0: 0x0044, 1: 0x0046}[self.slot_idx] # config70
+ self.lanes_ids_2 = {0: 0x190A, 1: 0x110A}[self.slot_idx] # config71
+ self.octetpath_sel = {0: 0x0123, 1: 0x0132}[self.slot_idx] # config95
+
+ self.init()
+
+
+ def tx_enable(self, enable=False):
+ """
+ Enable/disable the analog TX output.
+ """
+ enable_bit = 0b1 if enable else 0b0
+ prev_val = self.regs.peek16(0x03)
+ self.regs.poke16(0x03, prev_val | enable_bit)
+
+
+ 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.regs.poke16(addr, val)
+
+
+ def init(self):
+ """
+ Basic init that disables the analog output.
+ """
+ self.tx_enable(False) # Set TXENABLE low at the DAC
+ self.log.trace("DAC's Analog TX output is disabled.")
+
+ def reset(self):
+ """
+ Reset the DAC state
+ """
+ self.regs.poke16(0x02, 0x2002) # Deassert the reset for the SIF registers
+ self.regs.poke16(0x02, 0x2003) # Assert the reset for the SIF registers
+
+ def config(self):
+ """
+ Check the clock status, and write configuration values!
+ """
+ def _check_pll_lock():
+ pll_ool_alarms = self.regs.peek16(0x6C)
+ if (pll_ool_alarms & 0x0008) != 0x0000:
+ self.log.warning("PLL reporting unlocked... Status: 0x{:x}"
+ .format(pll_ool_alarms))
+ return False
+ return True
+
+ self.log.trace("Reset DAC & Clear alarm bits")
+ self.reset()
+ self.regs.poke16(0x6C, 0x0000) # Clear alarm bits for PLLs
+
+ self.log.trace("DAC Configuration.")
+ self.pokes16((
+ (0x00, 0x0018), # config0: Interpolation 1x; ALARM enabled w/ pos. logic.
+ (0x01, 0x0003), # config1: Rewriting reserved default values.
+ (0x02, 0x0002), # config2: Data not zero when link not established; 2's comp. arrives at input.
+ (0x03, 0x9300), # config3: Coarse DAC = 10 (9+1); TXENABLE internal is kept low.
+ (0x04, 0x0000), # config4: Not masking any lane errors or FIFO flags.
+ (0x05, 0x0000), # config5: Not masking any SYSREF errors, PAPs, or PLL locks.
+ (0x06, 0x0000), # config6: Not masking any lane short test or loss of signal.
+ (0x08, 0x0000), # config8: DAC A offset correction set to zero (default).
+ (0x09, 0x0000), # config9: DAC B offset correction set to zero (default).
+ (0x0A, 0x0000), # config10: DAC C offset correction set to zero (default).
+ (0x0B, 0x0000), # config11: DAC D offset correction set to zero (default).
+ (0x0C, 0x0400), # config12: Default quadrature correction gain A (AB path).
+ (0x0D, 0x0400), # config13: Default coarse mixing options; default quadrature correction gain B (AB path).
+ (0x0E, 0x0400), # config14: Default quadrature correction gain A (CD path).
+ (0x0F, 0x0400), # config15: No output delays to the DACs; default quadrature correction gain A (CD path).
+ (0x10, 0x0000), # config16: Default QMC correction phase (AB path).
+ (0x11, 0x0000), # config17: Default QMC correction phase (AD path).
+ (0x12, 0x0000), # config18: Phase offset for NCO in DACAB path (default).
+ (0x13, 0x0000), # config19: Phase offset for NCO in DACAB path (default).
+ (0x14, 0x0000), # config20: Lower 16 bits of NCO Frequency adjust word for DACAB path (default).
+ (0x15, 0x0000), # config21: Middle 16 bits of NCO Frequency adjust word for DACAB path (default).
+ (0x16, 0x0000), # config22: Upper 16 bits of NCO Frequency adjust word for DACAB path (default).
+ (0x17, 0x0000), # config23: Lower 16 bits of NCO Frequency adjust word for DACCD path (default).
+ (0x18, 0x0000), # config24: Middle 16 bits of NCO Frequency adjust word for DACCD path (default).
+ (0x19, 0x0000), # config25: Upper 16 bits of NCO Frequency adjust word for DACCD path (default).
+ (0x1A, 0x0023), # config26: DAC PLL in sleep mode; DAC C & D in sleep mode.
+ (0x1B, 0x0000), # config27: Testing settings (default).
+ (0x1E, 0x2222), # config30: Sync source for the QMC offset and correction: SYSREF only (Based on VST2).
+ (0x1F, 0x2220), # config31: Sync source for mixers and NCO accums.: SYSREF only (Based on VST2).
+ (0x20, 0x0000), # config32: Sync source for the dithering, PA protection, and FIR filter blocks: none (Based on VST2).
+ (0x22, 0x1B1B), # config34: JESD and DAC routing paths (default).
+ (0x23, 0x01FF), # config35: SLEEP signal from pin allowed to reach all blocks, the pin is not even used.
+ (0x24, 0x0000), # config36: SYSREF syncs clock dividers: use no pulses yet.
+ (0x25, 0x2000), # config37: DACCLK divider to generate JESD clock: div2. (TI recommendation).
+ (0x26, 0x0000), # config38: Dithering disabled default).
+ (0x2D, 0x0000), # config45: Power amplifier protection settings (default).
+ (0x2E, 0xFFFF), # config46: Power amplifier protection threshold (default).
+ (0x2F, 0x0004), # config47: Default values.
+ (0x30, 0x0000), # config48: Constant value sent to DAC when sifdac_ena is asserted (default).
+ (0x31, 0x1000), # config49: DAC PLL in reset and disabled. DACCLK is 491.52 MHz since we bypass the PLL.
+ (0x32, 0x0000), # config50: DAC PLL's VCO feedback and prescaler divided (not used).
+ (0x33, 0x0000), # config51: DAC PLL VCO and CP settings (not used).
+ (0x34, 0x0000), # config52: SYNCB electrical configuration (default @ 1.2V CMV).
+ (0x3B, 0x0000), # config59: SerDes PLL's reference. Source: DACCLK / Divider: 1 (0 +1).
+ (0x3C, 0x1828), # config60: SerDes PLL Control: high loop BW; high range VCO; multiply factor x5.
+ (0x3D, 0x0088), # config61: Upper configuration info for SerDes receivers (TI recommendation).
+ (0x3E, 0x0128), # config62: Upper configuration for SerDes receivers:AC coupling; half rate; 20-bit width.
+ (0x3F, 0x0000), # config63: No SerDes lanes inversion (default).
+ (0x46, self.lanes_ids_1), # config70: JESD ID for lanes 0, 1, and 2.
+ (0x47, self.lanes_ids_2), # config71: JESD ID for lanes 3, 4, and 5.
+ (0x48, 0x31C3), # config72: JESD ID for lanes 6, and 7; JESD204B supported version and class (default).
+ (0x49, 0x0000), # config73: JESD lanes assignment to links (default 0).
+ (0x4A, 0x0F3E), # config74: Lanes 0-3 enabled; test seq. disabled; disable clocks to C/D paths.
+ (0x4B, 0x1700), # config75: RBD = 24 (23 + 1) (Release Buffer Delay); F = 1 (octets per frame).
+ (0x4C, 0x1703), # config76: K = 24 (23 + 1) (frames in multiframe); L = 4 (3 + 1) (number of lanes).
+ (0x4D, 0x0100), # config77: M = 2 (1+1) (number of converters); S = 1 (0+1) (number of samples per frame).
+ (0x4E, 0x0F4F), # config78: HD = 1 (High Density mode enabled, samples split across lanes).
+ (0x4F, 0x1CC1), # config79: Match /R/ char; ILA is supported at TX.
+ (0x50, 0x0000), # config80: Lane config data (link0), not used by 37J82 (default).
+ (0x51, 0x00FF), # config81: Erros that cause a SYNC request (link0): all selected (default).
+ (0x52, 0x00FF), # config82: Errors that are counted in err_c (link0): all selected (default).
+ (0x53, 0x0000), # config83: Lane config data (link1), not used by 37J82 (default).
+ (0x54, 0x0000), # config84: Erros that cause a SYNC request (link1): none selected.
+ (0x55, 0x0000), # config85: Errors that are counted in err_c (link1): none selected.
+ (0x56, 0x0000), # config86: Lane config data (link2), not used by 37J82 (default).
+ (0x57, 0x0000), # config87: Erros that cause a SYNC request (link2): none selected.
+ (0x58, 0x0000), # config88: Errors that are counted in err_c (link2): none selected.
+ (0x59, 0x0000), # config89: Lane config data (link3), not used by 37J82 (default).
+ (0x5A, 0x0000), # config90: Erros that cause a SYNC request (link2): none selected.
+ (0x5B, 0x0000), # config91: Errors that are counted in err_c (link3): none selected.
+ (0x5C, 0x0000), # config92: Links 3:1 don't use SYSREF pulses; link 0 uses no pulses yet.
+ (0x5E, 0x0000), # config94: Cheksum bits for ILA, not used in 37J82 (default).
+ (0x5F, self.octetpath_sel), # config95: Mapping SerDes lanes (0-3) to JESD lanes.
+ (0x60, 0x4567), # config96: Mapping SerDes lanes (4-7) to JESD lanes (default).
+ (0x61, 0x0001), # config97: Use only link 0 to trigger the SYNCB LVDS output.
+ (0x64, 0x0703), # config100: Write to lane 0 errors to clear them (based on VST2).
+ (0x65, 0x0703), # config101: Write to lane 1 errors to clear them (based on VST2).
+ (0x66, 0x0703), # config102: Write to lane 2 errors to clear them (based on VST2).
+ (0x67, 0x0703), # config103: Write to lane 3 errors to clear them (based on VST2).
+ (0x68, 0x0703), # config104: Write to lane 4 errors to clear them (based on VST2).
+ (0x69, 0x0703), # config105: Write to lane 5 errors to clear them (based on VST2).
+ (0x6A, 0x0703), # config106: Write to lane 6 errors to clear them (based on VST2).
+ (0x6B, 0x0703), # config107: Write to lane 7 errors to clear them (based on VST2).
+ (0x6C, 0x0000), # config108: Rewrite the PLLs alarm bits clearing register.
+ (0x6D, 0x0000), # config109: JESD short test alarms (default).
+ (0x6E, 0x0000), # config110: Delay fractional filter settings (default).
+ (0x6F, 0x0000), # config111: Delay fractional filter settings (default).
+ (0x70, 0x0000), # config112: Delay fractional filter settings (default).
+ (0x71, 0x0000), # config113: Delay fractional filter settings (default).
+ (0x72, 0x0000), # config114: Delay fractional filter settings (default).
+ (0x73, 0x0000), # config115: Delay fractional filter settings (default).
+ (0x74, 0x0000), # config116: Delay fractional filter settings (default).
+ (0x75, 0x0000), # config117: Delay fractional filter settings (default).
+ (0x76, 0x0000), # config118: Delay fractional filter settings (default).
+ (0x77, 0x0000), # config119: Delay fractional filter settings (default).
+ (0x78, 0x0000), # config120: Delay fractional filter settings (default).
+ (0x79, 0x0000), # config121: Delay fractional filter settings (default).
+ (0x7A, 0x0000), # config122: Delay fractional filter settings (default).
+ (0x7B, 0x0000), # config123: Delay fractional filter settings (default).
+ (0x7C, 0x0000), # config124: Delay fractional filter settings (default).
+ (0x7D, 0x0000), # config125: Delay fractional filter settings (default).
+ (0x02, 0x2002), # Deassert the reset for the SIF registers
+ ))
+ self.log.trace("DAC register dump finished.")
+
+ self.log.trace("Polling for PLL lock...")
+ locked = False
+ for _ in range(6):
+ time.sleep(0.001)
+ # Clear stickies possibly?
+ self.regs.poke16(0x6C, 0x0000) # Clear alarm bits for PLLs
+ if _check_pll_lock():
+ locked = True
+ self.log.info("DAC PLL Locked!")
+ break
+ if not locked:
+ raise RuntimeError("DAC PLL did not lock! Check the logs for details.")
+
+
+ def enable_sysref_capture(self, enabled=False):
+ """
+ Enable the SYSREF capture block, and enable divider's reset.
+ """
+ self.log.trace("%s DAC SYSREF capture...",
+ {True: 'Enabling', False: 'Disabling'}[enabled])
+ cdrvser_sysref_mode = 0b001 if enabled else 0b000
+ sysref_mode_link0 = 0b001 if enabled else 0b000
+ self.regs.poke16(0x24, cdrvser_sysref_mode << 4) # Enable next SYSREF to reset the clock dividers.
+ self.regs.poke16(0x5C, sysref_mode_link0 << 0) # Enable next SYSREF pulse capture for link 0.
+ self.log.trace("DAC SYSREF capture %s." % {True: 'enabled', False: 'disabled'}[enabled])
+
+
+ def init_deframer(self):
+ """
+ Initialize the DAC's framer.
+ """
+ self.log.trace("Initializing framer...")
+ self.pokes16((
+ (0x4A, 0x0F3F), # config74: Deassert JESD204B block reset.
+ (0x4A, 0x0F21), # config74: Set JESD204B to exit init state.
+ ))
+ self.log.trace("DAC deframer initialized.")
+
+
+ def check_deframer_status(self):
+ """
+ This function checks the status of the framer by checking alarms.
+ """
+ ALARM_ERRORS_DESCRIPTION = {
+ 15 : "Multiframe alignment error",
+ 14 : "Frame alignment error",
+ 13 : "Link configuration error",
+ 12 : "Elastic buffer overflow",
+ 11 : "Elastic buffer match error",
+ 10 : "Code synchronization error",
+ 9 : "8b/10b not-in-table code error",
+ 8 : "8b/10b disparity error",
+ 3 : "FIFO write_error",
+ 2 : "FIFO write_full",
+ 1 : "FIFO read_error",
+ 0 : "FIFO read_empty"
+ }
+
+ self.log.trace("Checking DAC's deframer status.")
+ # Clear lane alarms.
+ for addr in (0x64, 0x65, 0x66, 0x67):
+ self.regs.poke16(addr, 0x0000)
+ time.sleep(0.001)
+
+ # Read lane's alarms
+ lanes_alarms_ary = []
+ lane = 0
+ for addr in (0x64, 0x65, 0x66, 0x67):
+ lanes_alarms_ary.insert(lane, self.regs.peek16(addr) & 0xFF0F)
+ self.log.trace("Lane {} alarms rb: 0x{:X}".format(lane, lanes_alarms_ary[lane]))
+ lane += 1
+
+ enable_analog_output = True
+ # Report warnings based on an error matrix (register_width * lanes).
+ errors_ary = []
+ for error in range(0, 16):
+ errors_ary.insert(error, [])
+ # Extract errors from lanes.
+ for lane in range(0, len(lanes_alarms_ary)):
+ if lanes_alarms_ary[lane] & (0b1 << error) > 0:
+ errors_ary[error].append(lane)
+ if len(errors_ary[error]) > 0:
+ enable_analog_output = False
+ self.log.warning(ALARM_ERRORS_DESCRIPTION[error] +
+ " in lane(s): " + ' '.join(map(str, errors_ary[error])))
+
+ self.tx_enable(enable_analog_output)
+ self.log.debug("%s analog TX output.",
+ {True: 'Enabling', False: 'Disabling'}[enable_analog_output])
+ return enable_analog_output
+
+
+ def test_mode(self, mode='PRBS-31', lane=0):
+ """
+ This method enables the DAC's test mode to verify the SerDes.
+ Users should monitor the ALARM pin to see the results of the test.
+ If the test is failing, ALARM will be high (or toggling if marginal).
+ If the test is passing, the ALARM will be low.
+ """
+ MODE_VAL = {'OFF': 0x0, 'PRBS-7': 0x2, 'PRBS-23': 0x3, 'PRBS-31': 0x4}
+ assert mode.upper() in MODE_VAL
+ assert lane in range(0, 8)
+ self.log.debug("Setting test mode for lane {} at the DAC: {}.".format(lane, mode))
+ # To run the PRBS test on the DAC, users first need to setup the DAC for
+ # normal use, then make the following SPI writes:
+ # 1. config74, set bits 4:0 to 0x1E to disable JESD clock.
+ addr = 0x4A
+ rb = self.regs.peek16(addr)
+ data_w = (rb & ~0x001F) | 0x001E if mode != 'OFF' else 0x0F3E
+ self.log.trace("Writing register {:02X} with {:04X}".format(addr, data_w))
+ self.regs.poke16(addr, data_w)
+ # 2. config61, set bits 14:12 to 0x2 to enable the 7-bit PRBS test pattern; or
+ # set bits 14:12 to 0x3 to enable the 23-bit PRBS test pattern; or
+ # set bits 14:12 to 0x4 to enable the 31-bit PRBS test pattern.
+ addr = 0x3D
+ rb = self.regs.peek16(addr)
+ data_w = (rb & ~0x7000) | (MODE_VAL[mode] << 12)
+ self.log.trace("Writing register {:02X} with {:04X}".format(addr, data_w))
+ self.regs.poke16(addr, data_w)
+ # 3. config27, set bits 11:8 to 0x3 to output PRBS testfail on ALARM pin.
+ # 4. config27, set bits 14:12 to the lane to be tested (0 through 7).
+ addr = 0x1B
+ rb = self.regs.peek16(addr)
+ data_w = (rb & ~0x7F00) | (0x3 << 8) | (lane << 12) if mode != 'OFF' else 0x0000
+ self.log.trace("Writing register {:02X} with {:04X}".format(addr, data_w))
+ self.regs.poke16(addr, data_w)
+ # 5. config62, make sure bits 12:11 are set to 0x0 to disable character alignment.
+ addr = 0x3E
+ rb = self.regs.peek16(addr)
+ if ((rb & 0x58) >> 11) != 0x0:
+ self.log.error("Char alignment is enabled when not expected.")
diff --git a/mpm/python/usrp_mpm/dboard_manager/gain_rh.py b/mpm/python/usrp_mpm/dboard_manager/gain_rh.py
new file mode 100644
index 000000000..978b8158b
--- /dev/null
+++ b/mpm/python/usrp_mpm/dboard_manager/gain_rh.py
@@ -0,0 +1,93 @@
+#
+# Copyright 2018 Ettus Research, a National Instruments Company
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+"""
+Gain table control for Rhodium
+"""
+
+from __future__ import print_function
+from usrp_mpm.dboard_manager.gaintables_rh import RX_LOWBAND_GAIN_TABLE
+from usrp_mpm.dboard_manager.gaintables_rh import RX_HIGHBAND_GAIN_TABLE
+from usrp_mpm.dboard_manager.gaintables_rh import TX_LOWBAND_GAIN_TABLE
+from usrp_mpm.dboard_manager.gaintables_rh import TX_HIGHBAND_GAIN_TABLE
+
+#from usrp_mpm.dboard_manager.rhodium import Rhodium
+
+###############################################################################
+# Constants
+###############################################################################
+
+GAIN_TABLE_MIN_INDEX = 0
+GAIN_TABLE_MAX_INDEX = 60
+DSA1_MIN_INDEX = 0
+DSA1_MAX_INDEX = 30
+DSA2_MIN_INDEX = 0
+DSA2_MAX_INDEX = 30
+
+GAIN_TBL_SEL_ADDR = 6
+GAIN_TBL_SEL_TX_SHIFT = 8
+GAIN_TBL_SEL_RX_SHIFT = 0
+GAIN_TBL_SEL_HIGH_BAND = 1
+GAIN_TBL_SEL_LOW_BAND = 0
+
+# convenience data values for GAIN_TBL_SEL
+GAIN_TBL_SEL_DATA_BOTH_HIGH = \
+ (GAIN_TBL_SEL_HIGH_BAND << GAIN_TBL_SEL_TX_SHIFT) | \
+ (GAIN_TBL_SEL_HIGH_BAND << GAIN_TBL_SEL_RX_SHIFT)
+GAIN_TBL_SEL_DATA_BOTH_LOW = \
+ (GAIN_TBL_SEL_LOW_BAND << GAIN_TBL_SEL_TX_SHIFT) | \
+ (GAIN_TBL_SEL_LOW_BAND << GAIN_TBL_SEL_RX_SHIFT)
+
+###############################################################################
+# Main class
+###############################################################################
+
+class GainTableRh():
+ """
+ CPLD gain table loader for Rhodium daughterboards
+ """
+ def __init__(self, cpld_regs, gain_tbl_regs, parent_log=None):
+ self.log = parent_log.getChild("CPLDGainTbl") if parent_log is not None \
+ else get_logger("CPLDGainTbl")
+ self.cpld_regs = cpld_regs
+ self.gain_tbl_regs = gain_tbl_regs
+ assert hasattr(self.cpld_regs, 'poke16')
+ assert hasattr(self.gain_tbl_regs, 'poke16')
+
+ def _load_default_table(self, table, gain_table):
+ def _create_spi_loader_message(table, index, dsa1, dsa2):
+ addr = 0
+ data = 0
+ if table == "rx":
+ tableindex = 1
+ elif table == "tx":
+ tableindex = 2
+ else:
+ raise RuntimeError("Invalid table selected in gain loader: " + table)
+ addr |= (tableindex << 6)
+ addr |= (index << 0)
+ data |= (dsa1 << 5)
+ data |= (dsa2 << 0)
+ return addr, data
+ for i in range(GAIN_TABLE_MIN_INDEX, GAIN_TABLE_MAX_INDEX):
+ addr, data = _create_spi_loader_message(
+ table,
+ i,
+ gain_table[i][0],
+ gain_table[i][1])
+ self.gain_tbl_regs.poke16(addr, data)
+
+ def init(self):
+ """
+ Loads the default gain table values to the CPLD via SPI
+ """
+ self.log.trace("Loading gain tables to CPLD")
+ self.cpld_regs.poke16(GAIN_TBL_SEL_ADDR, GAIN_TBL_SEL_DATA_BOTH_HIGH)
+ self._load_default_table("rx", RX_HIGHBAND_GAIN_TABLE)
+ self._load_default_table("tx", TX_HIGHBAND_GAIN_TABLE)
+ self.cpld_regs.poke16(GAIN_TBL_SEL_ADDR, GAIN_TBL_SEL_DATA_BOTH_LOW)
+ self._load_default_table("rx", RX_LOWBAND_GAIN_TABLE)
+ self._load_default_table("tx", TX_LOWBAND_GAIN_TABLE)
+ self.log.trace("Gain tables loaded")
diff --git a/mpm/python/usrp_mpm/dboard_manager/gaintables_rh.py b/mpm/python/usrp_mpm/dboard_manager/gaintables_rh.py
new file mode 100644
index 000000000..9e2178006
--- /dev/null
+++ b/mpm/python/usrp_mpm/dboard_manager/gaintables_rh.py
@@ -0,0 +1,271 @@
+#
+# Copyright 2018 Ettus Research, a National Instruments Company
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+"""
+Gain table constants for Rhodium
+"""
+
+###############################################################################
+# Constants
+###############################################################################
+
+# Rhodium has two configurable gain elements
+# DSA1 - 0-30, attenuation
+# DSA2 - 0-30, attenuation
+# Gain table values are written as [DSA1, DSA2]
+# This is stored as a gain table, so index 0 is the lowest available power
+# These default gain tables have 61 indices
+
+RX_LOWBAND_GAIN_TABLE = [
+ [30, 30],
+ [29, 30],
+ [28, 30],
+ [27, 30],
+ [26, 30],
+ [25, 30],
+ [24, 30],
+ [23, 30],
+ [22, 30],
+ [21, 30],
+ [20, 30],
+ [19, 30],
+ [18, 30],
+ [17, 30],
+ [16, 30],
+ [15, 30],
+ [14, 30],
+ [13, 30],
+ [12, 30],
+ [11, 30],
+ [10, 30],
+ [ 9, 30],
+ [ 9, 29],
+ [ 8, 29],
+ [ 8, 28],
+ [ 7, 28],
+ [ 7, 27],
+ [ 6, 27],
+ [ 6, 26],
+ [ 5, 26],
+ [ 5, 25],
+ [ 5, 24],
+ [ 5, 23],
+ [ 5, 22],
+ [ 5, 21],
+ [ 5, 20],
+ [ 5, 19],
+ [ 5, 18],
+ [ 5, 17],
+ [ 5, 16],
+ [ 5, 15],
+ [ 4, 15],
+ [ 4, 14],
+ [ 3, 14],
+ [ 3, 13],
+ [ 2, 13],
+ [ 2, 12],
+ [ 1, 12],
+ [ 1, 11],
+ [ 0, 11],
+ [ 0, 10],
+ [ 0, 9],
+ [ 0, 8],
+ [ 0, 7],
+ [ 0, 6],
+ [ 0, 5],
+ [ 0, 4],
+ [ 0, 3],
+ [ 0, 2],
+ [ 0, 1],
+ [ 0, 0]]
+
+RX_HIGHBAND_GAIN_TABLE = [
+ [30, 30],
+ [29, 30],
+ [28, 30],
+ [27, 30],
+ [26, 30],
+ [25, 30],
+ [24, 30],
+ [23, 30],
+ [22, 30],
+ [21, 30],
+ [20, 30],
+ [19, 30],
+ [18, 30],
+ [17, 30],
+ [16, 30],
+ [15, 30],
+ [14, 30],
+ [13, 30],
+ [12, 30],
+ [11, 30],
+ [10, 30],
+ [ 9, 30],
+ [ 8, 30],
+ [ 7, 30],
+ [ 7, 29],
+ [ 6, 29],
+ [ 5, 29],
+ [ 5, 28],
+ [ 4, 28],
+ [ 3, 28],
+ [ 3, 27],
+ [ 2, 27],
+ [ 2, 26],
+ [ 2, 25],
+ [ 1, 25],
+ [ 1, 24],
+ [ 1, 23],
+ [ 0, 23],
+ [ 0, 22],
+ [ 0, 21],
+ [ 0, 20],
+ [ 0, 19],
+ [ 0, 18],
+ [ 0, 17],
+ [ 0, 16],
+ [ 0, 15],
+ [ 0, 14],
+ [ 0, 13],
+ [ 0, 12],
+ [ 0, 11],
+ [ 0, 10],
+ [ 0, 9],
+ [ 0, 8],
+ [ 0, 7],
+ [ 0, 6],
+ [ 0, 5],
+ [ 0, 4],
+ [ 0, 3],
+ [ 0, 2],
+ [ 0, 1],
+ [ 0, 0]]
+
+TX_LOWBAND_GAIN_TABLE = [
+ [30, 30],
+ [29, 30],
+ [29, 29],
+ [28, 29],
+ [28, 28],
+ [27, 28],
+ [27, 27],
+ [26, 27],
+ [26, 26],
+ [25, 26],
+ [25, 25],
+ [24, 25],
+ [24, 24],
+ [23, 24],
+ [23, 23],
+ [22, 23],
+ [22, 22],
+ [21, 22],
+ [21, 21],
+ [20, 21],
+ [20, 20],
+ [19, 20],
+ [19, 19],
+ [18, 19],
+ [18, 18],
+ [17, 18],
+ [17, 17],
+ [16, 17],
+ [16, 16],
+ [15, 16],
+ [15, 15],
+ [14, 15],
+ [14, 14],
+ [13, 14],
+ [13, 13],
+ [12, 13],
+ [12, 12],
+ [11, 12],
+ [11, 11],
+ [10, 11],
+ [10, 10],
+ [ 9, 10],
+ [ 9, 9],
+ [ 8, 9],
+ [ 8, 8],
+ [ 7, 8],
+ [ 7, 7],
+ [ 6, 7],
+ [ 6, 6],
+ [ 5, 6],
+ [ 5, 5],
+ [ 4, 5],
+ [ 4, 4],
+ [ 3, 4],
+ [ 3, 3],
+ [ 2, 3],
+ [ 2, 2],
+ [ 1, 2],
+ [ 1, 1],
+ [ 0, 1],
+ [ 0, 0]]
+
+TX_HIGHBAND_GAIN_TABLE = [
+ [30, 30],
+ [29, 30],
+ [29, 29],
+ [28, 29],
+ [28, 28],
+ [27, 28],
+ [27, 27],
+ [26, 27],
+ [26, 26],
+ [25, 26],
+ [25, 25],
+ [24, 25],
+ [24, 24],
+ [23, 24],
+ [23, 23],
+ [22, 23],
+ [22, 22],
+ [21, 22],
+ [21, 21],
+ [20, 21],
+ [20, 20],
+ [19, 20],
+ [19, 19],
+ [18, 19],
+ [18, 18],
+ [17, 18],
+ [17, 17],
+ [16, 17],
+ [16, 16],
+ [15, 16],
+ [15, 15],
+ [14, 15],
+ [14, 14],
+ [13, 14],
+ [13, 13],
+ [12, 13],
+ [12, 12],
+ [11, 12],
+ [11, 11],
+ [10, 11],
+ [10, 10],
+ [ 9, 10],
+ [ 9, 9],
+ [ 8, 9],
+ [ 8, 8],
+ [ 7, 8],
+ [ 7, 7],
+ [ 6, 7],
+ [ 6, 6],
+ [ 5, 6],
+ [ 5, 5],
+ [ 4, 5],
+ [ 4, 4],
+ [ 3, 4],
+ [ 3, 3],
+ [ 2, 3],
+ [ 2, 2],
+ [ 1, 2],
+ [ 1, 1],
+ [ 0, 1],
+ [ 0, 0]]
diff --git a/mpm/python/usrp_mpm/dboard_manager/lmk_rh.py b/mpm/python/usrp_mpm/dboard_manager/lmk_rh.py
new file mode 100644
index 000000000..288f03864
--- /dev/null
+++ b/mpm/python/usrp_mpm/dboard_manager/lmk_rh.py
@@ -0,0 +1,316 @@
+#
+# Copyright 2018 Ettus Research, a National Instruments Company
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+"""
+LMK04828 driver for use with Rhodium
+"""
+
+import time
+from ..mpmlog import get_logger
+from ..chips import LMK04828
+
+class LMK04828Rh(LMK04828):
+ """
+ This class provides an interface to configure the LMK04828 IC through SPI.
+ """
+
+ def __init__(self, slot_idx, regs_iface, ref_clock_freq, sampling_clock_freq, parent_log=None):
+ self.log = \
+ parent_log.getChild("LMK04828-{}".format(slot_idx)) if parent_log is not None \
+ else get_logger("LMK04828-{}".format(slot_idx))
+ LMK04828.__init__(self, regs_iface, parent_log)
+ self.log.debug("Using reference clock frequency {} MHz".format(ref_clock_freq/1e6))
+ self.log.debug("Using sampling clock frequency: {} MHz".format(sampling_clock_freq/1e6))
+ self.ref_clock_freq = ref_clock_freq
+ self.sampling_clock_freq = sampling_clock_freq
+ # VCXO on Rh runs at 122.88 MHz
+ self.vcxo_freq = 122.88e6
+ self.clkin_r_divider = { 10e6: 250, 20e6: 500, 25e6: 625, 30.72e6: 750}[self.ref_clock_freq]
+ self.pll1_n_divider = { 10e6: 3072, 20e6: 3072, 25e6: 3072, 30.72e6: 3000}[self.ref_clock_freq]
+ self.pll2_r_divider = {400e6: 32, 491.52e6: 32, 500e6: 128}[self.sampling_clock_freq]
+ self.pll2_prescaler = {400e6: 5, 491.52e6: 2, 500e6: 5}[self.sampling_clock_freq]
+ self.pll2_n_divider = {400e6: 125, 491.52e6: 320, 500e6: 625}[self.sampling_clock_freq]
+ self.pll2_vco_freq = (self.vcxo_freq/self.pll2_r_divider)*self.pll2_prescaler*self.pll2_n_divider
+ self.vco_selection = self.get_vco_selection()
+ self.sysref_divider = {400e6: 144, 491.52e6: 120, 500e6: 144}[self.sampling_clock_freq]
+ self.fpga_sysref_dly = {400e6: 11, 491.52e6: 8, 500e6: 10}[self.sampling_clock_freq]
+ self.clkout_divider = {400e6: 6, 491.52e6: 5, 500e6: 6}[self.sampling_clock_freq]
+ self.lb_lo_divider = {400e6: 2, 491.52e6: 2, 500e6: 2}[self.sampling_clock_freq]
+ self.log.trace("Variable Configuration Report: "
+ "clkin0/1_r = 0d{}, clkout_div = 0d{}, pll1_n = 0d{}"
+ .format(self.clkin_r_divider, self.clkout_divider, self.pll1_n_divider))
+ self.log.trace("Variable Configuration Report: "
+ "sysref_divider = 0d{}, fpga_sysref_dly = 0d{},"
+ .format(self.sysref_divider, self.fpga_sysref_dly))
+ self.log.trace("Variable Configuration Report: "
+ "pll2_pre = 0d{}, pll2_n = 0d{}, pll2_vco_freq = 0d{}"
+ .format(self.pll2_prescaler, self.pll2_n_divider, self.pll2_vco_freq))
+
+ self.init()
+ self.config()
+
+ def get_vco_freq(self):
+ """
+ Return the calculated VCO frequency in the LMK PLL2.
+ """
+ return self.pll2_vco_freq
+
+
+ def get_vco_selection(self):
+ """
+ The internal VCO (0/1) is selected depending on the pll2_vco_freq value.
+ According to the datasheet:
+ VCO0 Frequency -> 2370 to 2630 MHz (Default)
+ VCO1 Frequency -> 2920 to 3080 MHz
+ The VCO selection is configured with bits 6:5 (VCO_MUX) in register 0x138:
+ 0x00 | VCO 0
+ 0x01 | VCO 1
+ 0x01 | CLKin1 (ext)
+ 0x03 | Reserved
+ This function returns the full register value with ONLY the VCO selected
+ (i.e. the returned value must be or-ed with the rest of the configuration).
+ """
+ if (self.pll2_vco_freq >= 2370e6) and (self.pll2_vco_freq <= 2630e6):
+ self.log.trace("VCO0 selected for PLL2.")
+ return 0x00 << 5
+ elif (self.pll2_vco_freq >= 2920e6) and (self.pll2_vco_freq <= 3080e6):
+ self.log.trace("Internal VCO1 selected for PLL2.")
+ return 0x01 << 5
+ else:
+ self.log.error("The calculated PLL2 VCO frequency ({}) is not supported \
+ by neither internal VCO".format(self.pll2_vco_freq))
+ raise Exception("PLL2 VCO frequency not supported. Check log for details.")
+
+
+ def init(self):
+ """
+ Basic init. Turns it on. Enables SPI reads.
+ """
+ self.log.debug("Reset LMK & Verify")
+ self.pokes8((
+ (0x000, 0x80), # Assert reset
+ (0x000, 0x00), # De-assert reset
+ (0x002, 0x00), # De-assert power down
+ ))
+ if not self.verify_chip_id():
+ raise Exception("Unable to locate LMK04828")
+
+
+ def config(self):
+ """
+ Writes the entire configuration necessary for Rhodium operation.
+ """
+ self.log.trace("LMK Initialization")
+
+ # The sampling clocks going to the converters must be set at the sampling_clock_freq
+ # rate. But, the JESD204B IP at the FPGA expects the clocks to be half that rate;
+ # therefore, the actual divider for these clocks must be twice the normal.
+ convclk_div_val = self.divide_to_reg(self.clkout_divider)
+ convclk_cnt_val = self.divide_to_cnth_cntl_reg(self.clkout_divider)
+ fpgaclk_div_val = self.divide_to_reg(self.clkout_divider*2)
+ fpgaclk_cnt_val = self.divide_to_cnth_cntl_reg(self.clkout_divider*2)
+ # The LMK provides both the TX and RX low-band references, which are configured
+ # with the same divider values.
+ lb_lo_div_val = self.divide_to_reg(self.lb_lo_divider)
+ lb_lo_cnt_val = self.divide_to_cnth_cntl_reg(self.lb_lo_divider)
+
+ # Determine one of the SDCLkOut configuration registers (0x104) for the SYSREF
+ # signal going to the FPGA.
+ # SYSREF out VCO cycles to delay SYSREF
+ fpga_sysref_config = (0b1 << 5) | ((self.fpga_sysref_dly - 1) << 1)
+ self.log.trace("FPGA SYSREF delay register (0x104): 0x{:02X}".format(fpga_sysref_config))
+
+ self.pokes8((
+ (0x100, fpgaclk_div_val), # CLKout Config (FPGA Clock)
+ (0x101, fpgaclk_cnt_val), # CLKout Config
+ (0x102, 0x88), # CLKout Config
+ (0x103, 0x00), # CLKout Config
+ (0x104, fpga_sysref_config), # CLKout Config
+ (0x105, 0x00), # CLKout Config
+ (0x106, 0x72), # CLKout Config
+ (0x107, 0x11), # CLKout Config
+ (0x108, fpgaclk_div_val), # CLKout Config (MGT Reference Clock)
+ (0x109, fpgaclk_cnt_val), # CLKout Config
+ (0x10A, 0x88), # CLKout Config
+ (0x10B, 0x00), # CLKout Config
+ (0x10C, 0x00), # CLKout Config
+ (0x10D, 0x00), # CLKout Config
+ (0x10E, 0xF1), # CLKout Config
+ (0x10F, 0x05), # CLKout Config
+ (0x110, convclk_div_val), # CLKout Config (DAC Clock)
+ (0x111, convclk_cnt_val), # CLKout Config
+ (0x112, 0x22), # CLKout Config
+ (0x113, 0x00), # CLKout Config
+ (0x114, 0x20), # CLKout Config
+ (0x115, 0x00), # CLKout Config
+ (0x116, 0x72), # CLKout Config
+ (0x117, 0x75), # CLKout Config
+ (0x118, lb_lo_div_val), # CLKout Config (TX LB LO)
+ (0x119, lb_lo_cnt_val), # CLKout Config
+ (0x11A, 0x11), # CLKout Config
+ (0x11B, 0x00), # CLKout Config
+ (0x11C, 0x00), # CLKout Config
+ (0x11D, 0x00), # CLKout Config
+ (0x11E, 0x71), # CLKout Config
+ (0x11F, 0x05), # CLKout Config
+ (0x120, fpgaclk_div_val), # CLKout Config (Test Point Clock)
+ (0x121, fpgaclk_cnt_val), # CLKout Config
+ (0x122, 0x22), # CLKout Config
+ (0x123, 0x00), # CLKout Config
+ (0x124, 0x20), # CLKout Config
+ (0x125, 0x00), # CLKout Config
+ (0x126, 0x72), # CLKout Config
+ (0x127, 0x15), # CLKout Config
+ (0x128, lb_lo_div_val), # CLKout Config (RX LB LO)
+ (0x129, lb_lo_cnt_val), # CLKout Config
+ (0x12A, 0x22), # CLKout Config
+ (0x12B, 0x00), # CLKout Config
+ (0x12C, 0x00), # CLKout Config
+ (0x12D, 0x00), # CLKout Config
+ (0x12E, 0x71), # CLKout Config
+ (0x12F, 0x85), # CLKout Config
+ (0x130, convclk_div_val), # CLKout Config (ADC Clock)
+ (0x131, convclk_cnt_val), # CLKout Config
+ (0x132, 0x22), # CLKout Config
+ (0x133, 0x00), # CLKout Config
+ (0x134, 0x20), # CLKout Config
+ (0x135, 0x00), # CLKout Config
+ (0x136, 0x72), # CLKout Config
+ (0x137, 0x55), # CLKout Config
+ (0x138, (0x04 | self.vco_selection)), # VCO_MUX to VCO 0; OSCin->OSCout @ LVPECL 1600 mV
+ (0x139, 0x00), # SYSREF Source = MUX; SYSREF MUX = Normal SYNC
+ (0x13A, (self.sysref_divider & 0x1F00) >> 8), # SYSREF Divide [12:8]
+ (0x13B, (self.sysref_divider & 0x00FF) >> 0), # SYSREF Divide [7:0]
+ (0x13C, 0x00), # SYSREF DDLY [12:8]
+ (0x13D, 0x0A), # SYSREF DDLY [7:0] ... 8 is default, <8 is reserved
+ (0x13E, 0x00), # SYSREF Pulse Count = 1 pulse/request
+ (0x13F, 0x00), # Feedback Mux: Disabled. OSCin, drives PLL1N divider (Dual PLL non 0-delay). PLL2_P drives PLL2N divider.
+ (0x140, 0x00), # POWERDOWN options
+ (0x141, 0x00), # Dynamic digital delay enable
+ (0x142, 0x00), # Dynamic digital delay step
+ (0x143, 0xD1), # SYNC edge sensitive; SYSREF_CLR; SYNC Enabled; SYNC from pin no pulser
+ (0x144, 0x00), # Enable SYNC on all outputs including sysref
+ (0x145, 0x7F), # Always program to d127
+ (0x146, 0x00), # CLKin Type & En
+ (0x147, 0x0A), # CLKin_SEL = CLKin0 manual (10/20/25 MHz) / CLKin1 manual (30.72 MHz); CLKin0/1 to PLL1
+ (0x148, 0x02), # CLKin_SEL0 = input /w pulldown (default)
+ (0x149, 0x02), # CLKin_SEL1 = input w/ pulldown +SDIO RDBK (push-pull)
+ (0x14A, 0x02), # RESET type: in. w/ pulldown (default)
+ (0x14B, 0x02), # Holdover & DAC Manual Mode
+ (0x14C, 0x00), # DAC Manual Mode
+ (0x14D, 0x00), # DAC Settings (defaults)
+ (0x14E, 0x00), # DAC Settings (defaults)
+ (0x14F, 0x7F), # DAC Settings (defaults)
+ (0x150, 0x00), # Holdover Settings; bit 1 = '0' per long PLL1 lock time debug
+ (0x151, 0x02), # Holdover Settings (defaults)
+ (0x152, 0x00), # Holdover Settings (defaults)
+ (0x153, (self.clkin_r_divider & 0x3F00) >> 8), # CLKin0_R divider [13:8], default = 0
+ (0x154, (self.clkin_r_divider & 0x00FF) >> 0), # CLKin0_R divider [7:0], default = d120
+ (0x155, (self.clkin_r_divider & 0x3F00) >> 8), # CLKin1_R divider [13:8], default = 0
+ (0x156, (self.clkin_r_divider & 0x00FF) >> 0), # CLKin1_R divider [7:0], default = d150
+ (0x157, 0x00), # CLKin2_R divider [13:8], default = 0 (Not used)
+ (0x158, 0x01), # CLKin2_R divider [7:0], default = d1 (Not used)
+ (0x159, (self.pll1_n_divider & 0x3F00) >> 8), # PLL1 N divider [13:8], default = d6
+ (0x15A, (self.pll1_n_divider & 0x00FF) >> 0), # PLL1 N divider [7:0], default = d0
+ (0x15B, 0xC7), # PLL1 PFD: negative slope for active filter / CP = 750 uA
+ (0x15C, 0x27), # PLL1 DLD Count [13:8]
+ (0x15D, 0x10), # PLL1 DLD Count [7:0]
+ (0x15E, 0x00), # PLL1 R/N delay, defaults = 0
+ (0x15F, 0x0B), # Status LD1 pin = PLL1 LD, push-pull output
+ (0x160, (self.pll2_r_divider & 0x0F00) >> 8), # PLL2 R divider [11:8];
+ (0x161, (self.pll2_r_divider & 0x00FF) >> 0), # PLL2 R divider [7:0]
+ (0x162, self.pll2_pre_to_reg(self.pll2_prescaler)), # PLL2 prescaler; OSCin freq
+ (0x163, 0x00), # PLL2 Cal = PLL2 normal val
+ (0x164, 0x00), # PLL2 Cal = PLL2 normal val
+ (0x165, 0x0A), # PLL2 Cal = PLL2 normal val
+ (0x171, 0xAA), # Write this val after x165
+ (0x172, 0x02), # Write this val after x165
+ (0x17C, 0x15), # VCo1 Cal; write before x168
+ (0x17D, 0x33), # VCo1 Cal; write before x168
+ (0x166, (self.pll2_n_divider & 0x030000) >> 16), # PLL2 N[17:16]
+ (0x167, (self.pll2_n_divider & 0x00FF00) >> 8), # PLL2 N[15:8]
+ (0x168, (self.pll2_n_divider & 0x0000FF) >> 0), # PLL2 N[7:0]
+ (0x169, 0x59), # PLL2 PFD
+ (0x16A, 0x27), # PLL2 DLD Count [13:8] = default d39; SYSREF_REQ_EN disabled.
+ (0x16B, 0x10), # PLL2 DLD Count [7:0] = default d16
+ (0x16C, 0x00), # PLL2 Loop filter R3/4 = 200 ohm
+ (0x16D, 0x00), # PLL2 loop filter C3/4 = 10 pF
+ (0x16E, 0x13), # Status LD2 pin = Output push-pull, PLL2 DLD
+ (0x173, 0x00), # Do not power down PLL2 or prescaler
+ ))
+
+ # Poll for PLL1/2 lock. Total time = 10 * 100 ms = 1000 ms max.
+ self.log.trace("Polling for PLL lock...")
+ locked = False
+ for _ in range(10):
+ time.sleep(0.100)
+ # Clear stickies
+ self.pokes8((
+ (0x182, 0x1), # Clear Lock Detect Sticky
+ (0x182, 0x0), # Clear Lock Detect Sticky
+ (0x183, 0x1), # Clear Lock Detect Sticky
+ (0x183, 0x0), # Clear Lock Detect Sticky
+ ))
+ if self.check_plls_locked():
+ locked = True
+ self.log.trace("LMK PLLs Locked!")
+ break
+ if not locked:
+ raise RuntimeError("At least one LMK PLL did not lock! Check the logs for details.")
+
+ self.log.trace("Setting SYNC and SYSREF config...")
+ self.pokes8((
+ (0x143, 0xF1), # toggle SYNC polarity to trigger SYNC event
+ (0x143, 0xD1), # toggle SYNC polarity to trigger SYNC event
+ (0x139, 0x02), # SYSREF Source = MUX; SYSREF MUX = pulser
+ (0x144, 0xFF), # Disable SYNC on all outputs including sysref
+ (0x143, 0x52), # Pulser selected; SYNC enabled; 1 shot enabled
+ ))
+ self.log.info("LMK initialized and locked!")
+
+ def lmk_shift(self, num_shifts=0):
+ """
+ Apply time shift
+ """
+ self.log.trace("Clock Shifting Commencing using Dynamic Digital Delay...")
+ # The sampling clocks going to the converters are set at the sampling_clock_freq
+ # rate. But, the JESD204B IP at the FPGA expects the clocks to be half that rate;
+ # therefore, the actual divider for the FPGA clocks is twice the normal.
+ ddly_value_conv = self.divide_to_cnth_cntl_reg(self.clkout_divider+1) \
+ if num_shifts >= 0 else self.divide_to_cnth_cntl_reg(self.clkout_divider-1)
+ ddly_value_fpga = self.divide_to_cnth_cntl_reg(self.clkout_divider*2+1) \
+ if num_shifts >= 0 else self.divide_to_cnth_cntl_reg(self.clkout_divider*2-1)
+ ddly_value_sysref = self.sysref_divider+1 if num_shifts >= 0 else self.sysref_divider-1
+ # Since the LMK provides the low-band LO references for RX/TX, these need to be
+ # also shifted along with the sampling clocks to achieve the best possible
+ # phase alignment perfomance at this band.
+ ddly_value_lb_lo = self.divide_to_cnth_cntl_reg(self.lb_lo_divider+1) \
+ if num_shifts >= 0 else self.divide_to_cnth_cntl_reg(self.lb_lo_divider-1)
+ self.pokes8((
+ # Clocks to shift: 0(FPGA CLK), 4(DAC), 6(TX LB-LO), 8(Test), 10(RX LB-LO), 12(ADC)
+ (0x141, 0xFD), # Dynamic digital delay enable on outputs.
+ (0x143, 0x53), # SYSREF_CLR; SYNC Enabled; SYNC from pulser @ regwrite
+ (0x139, 0x02), # SYSREF_MUX = Pulser
+ (0x101, ddly_value_fpga), # Set DDLY values for DCLKout0 +/-1 on low cnt.
+ (0x102, ddly_value_fpga), # Hidden register. Write the same as previous based on inc/dec.
+ (0x111, ddly_value_conv), # Set DDLY values for DCLKout4 +/-1 on low cnt
+ (0x112, ddly_value_conv), # Hidden register. Write the same as previous based on inc/dec.
+ (0x119, ddly_value_lb_lo), # Set DDLY values for DCLKout6 +/-1 on low cnt
+ (0x11A, ddly_value_lb_lo), # Hidden register. Write the same as previous based on inc/dec.
+ (0x121, ddly_value_fpga), # Set DDLY values for DCLKout8 +/-1 on low cnt
+ (0x122, ddly_value_fpga), # Hidden register. Write the same as previous based on inc/dec.
+ (0x129, ddly_value_lb_lo), # Set DDLY values for DCLKout10 +/-1 on low cnt
+ (0x12A, ddly_value_lb_lo), # Hidden register. Write the same as previous based on inc/dec.
+ (0x131, ddly_value_conv), # Set DDLY values for DCLKout12 +/-1 on low cnt
+ (0x132, ddly_value_conv), # Hidden register. Write the same as previous based on inc/dec.
+ (0x13C, (ddly_value_sysref & 0x1F00) >> 8), # SYSREF DDLY value
+ (0x13D, (ddly_value_sysref & 0x00FF) >> 0), # SYSREF DDLY value
+ (0x144, 0x02), # Enable SYNC on outputs 0, 4, 6, 8, 10, 12
+ ))
+ for _ in range(abs(num_shifts)):
+ self.poke8(0x142, 0x1)
+ # Put everything back the way it was before shifting.
+ self.poke8(0x144, 0xFF) # Disable SYNC on all outputs including SYSREF
+ self.poke8(0x143, 0x52) # Pulser selected; SYNC enabled; 1 shot enabled
diff --git a/mpm/python/usrp_mpm/dboard_manager/rh_init.py b/mpm/python/usrp_mpm/dboard_manager/rh_init.py
new file mode 100644
index 000000000..b4d4dded6
--- /dev/null
+++ b/mpm/python/usrp_mpm/dboard_manager/rh_init.py
@@ -0,0 +1,481 @@
+#
+# Copyright 2018 Ettus Research, a National Instruments Company
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+"""
+Helper class to initialize a Rhodium daughterboard
+"""
+
+from __future__ import print_function
+import time
+from usrp_mpm.sys_utils.uio import UIO
+from usrp_mpm.dboard_manager.lmk_rh import LMK04828Rh
+from usrp_mpm.dboard_manager.rh_periphs import DboardClockControl
+from usrp_mpm.cores import ClockSynchronizer
+from usrp_mpm.cores import nijesdcore
+from usrp_mpm.cores.eyescan import EyeScanTool
+from usrp_mpm.dboard_manager.gain_rh import GainTableRh
+
+
+class RhodiumInitManager(object):
+ """
+ Helper class: Holds all the logic to initialize an N320/N321 (Rhodium)
+ daughterboard.
+ """
+ # After manually probing the PLL1's reference and feedback signal from the LMK
+ # using multiple phase dac values close to its midpoint (2^11 = 2048), it was
+ # discovered that the PLL1's tightest phase lock is at 2024.
+ INIT_PHASE_DAC_WORD = 32768 # TODO: update this number for Rev. B
+ PHASE_DAC_SPI_ADDR = 0x3
+ # External PPS pipeline delay from the PPS captured at the FPGA to TDC input,
+ # in reference clock ticks
+ EXT_PPS_DELAY = 5
+ # Variable PPS delay before the RP/SP pulsers begin. Fixed value for the N3xx devices.
+ N3XX_INT_PPS_DELAY = 4
+ # JESD core default configuration.
+ # TODO: define the actual values for rx_sysref_delay and tx_sysref_delay.
+ JESD_DEFAULT_ARGS = {"lmfc_divider" : 12,
+ "rx_sysref_delay": 8,
+ "tx_sysref_delay": 11,
+ "tx_driver_swing": 0b1101,
+ "tx_precursor" : 0b00100,
+ "tx_postcursor" : 0b00100}
+
+
+ def __init__(self, rh_class, spi_ifaces):
+ self.rh_class = rh_class
+ self._spi_ifaces = spi_ifaces
+ self.adc = rh_class.adc
+ self.dac = rh_class.dac
+ self.slot_idx = rh_class.slot_idx
+ self.log = rh_class.log.getChild('init')
+
+
+ def _init_lmk(self, lmk_spi, ref_clk_freq, sampling_clock_rate,
+ pdac_spi, init_phase_dac_word, phase_dac_spi_addr):
+ """
+ Sets the phase DAC to initial value, and then brings up the LMK
+ according to the selected ref clock frequency.
+ Will throw if something fails.
+ """
+ self.log.trace("Initializing Phase DAC to d{}.".format(
+ init_phase_dac_word
+ ))
+ pdac_spi.poke16(phase_dac_spi_addr, init_phase_dac_word)
+ return LMK04828Rh(self.slot_idx, lmk_spi, ref_clk_freq, sampling_clock_rate, self.log)
+
+
+ # TODO: update phase shift value after testing phase DAC flatness with shields (Rev. B)
+ def _sync_db_clock(self, dboard_ctrl_regs, ref_clk_freq, master_clock_rate, args):
+ " Synchronizes the DB clock to the common reference "
+ reg_offset = 0x200
+ ext_pps_delay = self.EXT_PPS_DELAY
+ if args.get('time_source', self.rh_class.default_time_source) == 'sfp0':
+ reg_offset = 0x400
+ ref_clk_freq = 62.5e6
+ ext_pps_delay = 1 # only 1 flop between the WR core output and the TDC input
+ synchronizer = ClockSynchronizer(
+ dboard_ctrl_regs,
+ self.rh_class.lmk,
+ self._spi_ifaces['phase_dac'],
+ reg_offset,
+ master_clock_rate,
+ ref_clk_freq,
+ 1.1E-12, # fine phase shift. TODO don't hardcode. This should live in the EEPROM
+ self.INIT_PHASE_DAC_WORD,
+ self.PHASE_DAC_SPI_ADDR,
+ ext_pps_delay,
+ self.N3XX_INT_PPS_DELAY,
+ self.slot_idx)
+ # The radio clock traces on the motherboard are 69 ps longer for Daughterboard B
+ # than Daughterboard A. We want both of these clocks to align at the converters
+ # on each board, so adjust the target value for DB B. This is an N3xx series
+ # peculiarity and will not apply to other motherboards.
+ trace_delay_offset = {0: 0.0e-0,
+ 1: 69.0e-12}[self.slot_idx]
+ offset_error = abs(synchronizer.run(
+ num_meas=[512, 128],
+ target_offset=trace_delay_offset))
+ if offset_error > 100e-12:
+ self.log.error("Clock synchronizer measured an offset of {:.1f} ps!".format(
+ offset_error*1e12
+ ))
+ self.log.warning("RuntimeError is not being thrown for Rhodium Rev. A")
+ # raise RuntimeError("Clock synchronizer measured an offset of {:.1f} ps!".format(
+ # offset_error*1e12
+ # ))
+ else:
+ self.log.debug("Residual synchronization error: {:.1f} ps.".format(
+ offset_error*1e12
+ ))
+ synchronizer = None
+ self.log.debug("Sample Clock Synchronization Complete!")
+
+
+ def set_jesd_rate(self, jesdcore, new_rate, current_jesd_rate, force=False):
+ """
+ Make the QPLL and GTX changes required to change the JESD204B core rate.
+ """
+ # The core is directly compiled for 500 MHz sample rate, which
+ # corresponds to a lane rate of 5.0 Gbps. The same QPLL and GTX settings apply
+ # for the 491.52 MHz sample rate.
+ #
+ # The lower non-LTE rate, 400 MHz, requires changes to the default configuration
+ # of the MGT components. This function performs the required changes in the
+ # following order (as recommended by UG476).
+ #
+ # 1) Modify any QPLL settings.
+ # 2) Perform the QPLL reset routine by pulsing reset then waiting for lock.
+ # 3) Modify any GTX settings.
+ # 4) Perform the GTX reset routine by pulsing reset and waiting for reset done.
+
+ assert new_rate in (4000e6, 4915.2e6, 5000e6)
+
+ # On first run, we have no idea how the FPGA is configured... so let's force an
+ # update to our rate.
+ force = force or (current_jesd_rate is None)
+
+ skip_drp = False
+ if not force:
+ # Current New Skip?
+ skip_drp = {4000.0e6 : {4000.0e6: True , 4915.2e6: False, 5000.0e6: False},
+ 4915.2e6 : {4000.0e6: False, 4915.2e6: True , 5000.0e6: True },
+ 5000.0e6 : {4000.0e6: False, 4915.2e6: True , 5000.0e6: True }}[current_jesd_rate][new_rate]
+
+ if skip_drp:
+ self.log.trace("Current lane rate is compatible with the new rate. Skipping "
+ "reconfiguration.")
+
+ # These are the only registers in the QPLL and GTX that change based on the
+ # selected line rate. The MGT wizard IP was generated for each of the rates and
+ # reference clock frequencies and then diffed to create this table.
+ QPLL_CFG = {4000.0e6: 0x6801C1, 4915.2e6: 0x680181, 5000.0e6: 0x0680181}[new_rate]
+ MGT_RX_CLK25_DIV = {4000.0e6: 8, 4915.2e6: 10, 5000.0e6: 10}[new_rate]
+ MGT_TX_CLK25_DIV = {4000.0e6: 8, 4915.2e6: 10, 5000.0e6: 10}[new_rate]
+
+ # 1-2) Do the QPLL first
+ if not skip_drp:
+ self.log.trace("Changing QPLL settings to support {} Gbps".format(new_rate/1e9))
+ jesdcore.set_drp_target('qpll', 0)
+ # QPLL_CONFIG is spread across two regs: 0x32 (dedicated) and 0x33 (shared)
+ reg_x32 = QPLL_CFG & 0xFFFF # [16:0] -> [16:0]
+ reg_x33 = jesdcore.drp_access(rd=True, addr=0x33)
+ reg_x33 = (reg_x33 & 0xF800) | ((QPLL_CFG >> 16) & 0x7FF) # [26:16] -> [11:0]
+ jesdcore.drp_access(rd=False, addr=0x32, wr_data=reg_x32)
+ jesdcore.drp_access(rd=False, addr=0x33, wr_data=reg_x33)
+
+ # Run the QPLL reset sequence and prep the MGTs for modification.
+ jesdcore.init()
+
+ # 3-4) And the 4 MGTs second
+ if not skip_drp:
+ self.log.trace("Changing MGT settings to support {} Gbps"
+ .format(new_rate/1e9))
+ for lane in range(4):
+ jesdcore.set_drp_target('mgt', lane)
+ # MGT_RX_CLK25_DIV is embedded with others in 0x11. The
+ # encoding for the DRP register value is one less than the
+ # desired value.
+ reg_x11 = jesdcore.drp_access(rd=True, addr=0x11)
+ reg_x11 = (reg_x11 & 0xF83F) | \
+ ((MGT_RX_CLK25_DIV-1 & 0x1F) << 6) # [10:6]
+ jesdcore.drp_access(rd=False, addr=0x11, wr_data=reg_x11)
+ # MGT_TX_CLK25_DIV is embedded with others in 0x6A. The
+ # encoding for the DRP register value is one less than the
+ # desired value.
+ reg_x6a = jesdcore.drp_access(rd=True, addr=0x6A)
+ reg_x6a = (reg_x6a & 0xFFE0) | (MGT_TX_CLK25_DIV-1 & 0x1F) # [4:0]
+ jesdcore.drp_access(rd=False, addr=0x6A, wr_data=reg_x6a)
+ self.log.trace("GTX settings changed to support {} Gbps"
+ .format(new_rate/1e9))
+ jesdcore.disable_drp_target()
+
+ self.log.trace("JESD204b Lane Rate set to {} Gbps!"
+ .format(new_rate/1e9))
+ return new_rate
+
+
+ def init_jesd(self, jesdcore, sampling_clock_rate):
+ """
+ Bringup the JESD links between the ADC, DAC, and the FPGA.
+ All clocks must be set up and stable before starting this routine.
+ """
+ jesdcore.check_core()
+
+ # JESD Lane Rate only depends on the sampling_clock_rate selection, since all
+ # other link parameters (LMFS,N) remain constant.
+ L = 4
+ M = 2
+ F = 1
+ S = 1
+ N = 16
+ new_rate = sampling_clock_rate * M * N * (10.0/8) / L / S
+ self.log.trace("Calculated JESD204B lane rate is {} Gbps".format(new_rate/1e9))
+ self.rh_class.current_jesd_rate = \
+ self.set_jesd_rate(jesdcore, new_rate, self.rh_class.current_jesd_rate)
+
+ self.log.trace("Setting up JESD204B TX blocks.")
+ jesdcore.init_framer() # Initialize FPGA's framer.
+ self.adc.init_framer() # Initialize ADC's framer.
+
+ self.log.trace("Enabling SYSREF capture blocks.")
+ self.dac.enable_sysref_capture(True) # Enable DAC's SYSREF capture.
+ self.adc.enable_sysref_capture(True) # Enable ADC's SYSREF capture.
+ jesdcore.enable_lmfc(True) # Enable FPGA's SYSREF capture.
+
+ self.log.trace("Setting up JESD204B DAC RX block.")
+ self.dac.init_deframer() # Initialize DAC's deframer.
+
+ self.log.trace("Sending SYSREF to all devices.")
+ jesdcore.send_sysref_pulse() # Send SYSREF to all devices.
+
+ self.log.trace("Setting up JESD204B FPGA RX block.")
+ jesdcore.init_deframer() # Initialize FPGA's deframer.
+
+ self.log.trace("Disabling SYSREF capture blocks.")
+ self.dac.enable_sysref_capture(False) # Disable DAC's SYSREF capture.
+ self.adc.enable_sysref_capture(False) # Disable ADC's SYSREF capture.
+ jesdcore.enable_lmfc(False) # Disable FPGA's SYSREF capture.
+
+ time.sleep(0.100) # Allow time for CGS/ILA.
+
+ self.log.trace("Verifying JESD204B link status.")
+ error_flag = False
+ if not jesdcore.get_framer_status():
+ self.log.error("JESD204b FPGA Core Framer is not synced!")
+ error_flag = True
+ if not self.dac.check_deframer_status():
+ self.log.error("DAC JESD204B Deframer is not synced!")
+ error_flag = True
+ if not self.adc.check_framer_status():
+ self.log.error("ADC JESD204B Framer is not synced!")
+ error_flag = True
+ if not jesdcore.get_deframer_status(True): # TODO: Remove the boolean argument!
+ self.log.error("JESD204B FPGA Core Deframer is not synced!")
+ error_flag = True
+ if error_flag:
+ raise RuntimeError('JESD204B Link Initialization Failed. See MPM logs for details.')
+ self.log.info("JESD204B Link Initialization & Training Complete")
+
+
+ def init(self, args):
+ """
+ Run the full initialization sequence. This will bring everything up
+ from scratch: The LMK, JESD cores, the AD9695, the DAC37J82, and
+ anything else that is clocking-related.
+ Depending on the settings, this can take a fair amount of time.
+ """
+ # Input validation on RX margin tests (@ FPGA and DAC)
+ # By accepting the rx_eyescan/tx_prbs argument being str or bool, one may
+ # request an eyescan measurement to be performed from either the USRP's
+ # shell (i.e. using --default-args) or from the host's MPM shell.
+ perform_rx_eyescan = False
+ if 'rx_eyescan' in args:
+ perform_rx_eyescan = (args['rx_eyescan'] == 'True') or (args['rx_eyescan'] == True)
+ if perform_rx_eyescan:
+ self.log.trace("Adding RX eye scan PMA enable to JESD args.")
+ self.JESD_DEFAULT_ARGS["enable_rx_eyescan"] = True
+ perform_tx_prbs = False
+ if 'tx_prbs' in args:
+ perform_tx_prbs = (args['tx_prbs'] == 'True') or (args['tx_prbs'] == True)
+
+ # Bringup Sequence.
+ # 1. Prerequisites (include opening mmaps)
+ # 2. Initialize LMK and bringup clocks.
+ # 3. Synchronize DB Clocks.
+ # 4. Initialize FPGA JESD IP.
+ # 5. DAC Configuration.
+ # 6. ADC Configuration.
+ # 7. JESD204B Initialization.
+ # 8. CPLD Gain Tables Initialization.
+
+ # 1. Prerequisites
+ # Open FPGA IP (Clock control and JESD core).
+ self.log.trace("Creating dboard clock control object")
+ db_clk_control = DboardClockControl(self.rh_class.radio_regs, self.log)
+ self.log.trace("Creating jesdcore object")
+ jesdcore = nijesdcore.NIJESDCore(self.rh_class.radio_regs, self.rh_class.slot_idx, **self.JESD_DEFAULT_ARGS)
+
+ self.log.trace("Creating gain table object...")
+ self.gain_table_loader = GainTableRh(
+ self._spi_ifaces['cpld'],
+ self._spi_ifaces['cpld_gain_loader'],
+ self.log)
+
+ # 2. Initialize LMK and bringup clocks.
+ # Disable FPGA MMCM's outputs, and assert its reset.
+ db_clk_control.reset_mmcm()
+ # Always place the JESD204b cores in reset before modifying the clocks,
+ # otherwise high power or erroneous conditions could exist in the FPGA!
+ jesdcore.reset()
+ # Configure and bringup the LMK's clocks.
+ self.log.trace("Initializing LMK...")
+ self.rh_class.lmk = self._init_lmk(
+ self._spi_ifaces['lmk'],
+ self.rh_class.ref_clock_freq,
+ self.rh_class.sampling_clock_rate,
+ self._spi_ifaces['phase_dac'],
+ self.INIT_PHASE_DAC_WORD,
+ self.PHASE_DAC_SPI_ADDR
+ )
+ self.log.trace("LMK Initialized!")
+ # Deassert FPGA's MMCM reset, poll for lock, and enable outputs.
+ db_clk_control.enable_mmcm()
+
+ # 3. Synchronize DB Clocks.
+ # The clock synchronzation driver receives the master_clock_rate, which for
+ # Rhodium is half the sampling_clock_rate.
+ self._sync_db_clock(
+ self.rh_class.radio_regs,
+ self.rh_class.ref_clock_freq,
+ self.rh_class.sampling_clock_rate / 2,
+ args)
+
+ # 4. DAC Configuration.
+ self.dac.config()
+
+ # 5. ADC Configuration.
+ self.adc.config()
+
+ # 6-7. JESD204B Initialization.
+ self.init_jesd(jesdcore, self.rh_class.sampling_clock_rate)
+ # [Optional] Perform RX eyescan.
+ if perform_rx_eyescan:
+ self.log.info("Performing RX eye scan on ADC to FPGA link...")
+ self._rx_eyescan(jesdcore, args)
+ # [Optional] Perform TX PRBS test.
+ if perform_tx_prbs:
+ self.log.info("Performing TX PRBS-31 test on FPGA to DAC link...")
+ self._tx_prbs_test(jesdcore, args)
+ jesdcore = None # We are done using the jesdcore at this point.
+
+ # 8. CPLD Gain Tables Initialization.
+ self.gain_table_loader.init()
+
+ return True
+
+
+ ##########################################################################
+ # JESD204B RX margin testing
+ ##########################################################################
+
+ def _rx_eyescan(self, jesdcore, args):
+ """
+ This function creates an eyescan object to perform this measurement with the
+ given configuration and lanes.
+
+ Parameters:
+ prescale -> Controls the prescaling of the sample count to keep both sample
+ count and error count in reasonable precision.
+ Valid values: from 0 to 31.
+ """
+ # The following constants must be defined according to GTs configuration
+ # for each project. For further details, refer to the eyescan.py file.
+ # For Rhodium, these parameters are based on the JESD core.
+ rxout_div = 2
+ rx_int_datawidth = 20
+ eq_mode = 'LPM'
+ # The following variables define the GTs to be scanned and the range of the
+ # measurement.
+ prescale = 0
+ scan_lanes = [0, 1, 2, 3]
+ hor_range = {'start':-32 , 'stop':32 , 'step': 2}
+ ver_range = {'start':-127, 'stop':127, 'step': 2}
+ # Set default configuration values for Rhodium when the user is not intentionally
+ # changing the constants/variables default values.
+ for key in ('rxout_div', 'rx_int_datawidth', 'eq_mode',
+ 'prescale', 'scan_lanes', 'hor_range', 'ver_range'):
+ if key not in args:
+ self.log.trace("Setting Rh default value for {0}... val: {1}"
+ .format(key, locals()[key]))
+ args[key] = locals()[key]
+ #
+ # Create an eyescan object.
+ assert jesdcore is not None
+ eyescan_tool = EyeScanTool(jesdcore, self.slot_idx, **args)
+ # Put the ADC in pseudorandom test mode.
+ adc_regs = self._spi_ifaces['adc']
+ # test_val = adc_regs.peek8(0x0550)
+ # adc_regs.poke8(0x0550, 0x05)
+ test_val = adc_regs.peek8(0x0573)
+ adc_regs.poke8(0x0573, 0x13)
+ # Perform eye scan on given lanes and range.
+ file_name = eyescan_tool.eyescan_full_scan(args['scan_lanes'],
+ args['hor_range'], args['ver_range'])
+ # Do some housekeeping...
+ # adc_regs.poke8(0x0550, test_val) # Enable normal operation.
+ adc_regs.poke8(0x0573, test_val) # Enable normal operation.
+ adc_regs.poke8(0x0000, 0x81) # Reset.
+ eyescan_tool = None
+ return file_name
+
+ def _tx_prbs_test(self, jesdcore, args):
+ """
+ This function allows to test the PRBS-31 pattern at the DAC.
+ """
+ def _test_lanes(**tx_settings):
+ """
+ This methods enables, monitors, and disables the PRBS-31 test.
+ """
+ results = []
+ jesdcore.adjust_tx_phy(**tx_phy_settings)
+ self.log.info("Testing TX PHY settings: tx_driver_swing=0b{0:04b}"
+ " tx_precursor=0b{1:05b}"
+ " tx_postcursor=0b{2:05b}"
+ .format(tx_phy_settings["tx_driver_swing"],
+ tx_phy_settings["tx_precursor"],
+ tx_phy_settings["tx_postcursor"]))
+ # Enable the GTs TX pattern generator in PRBS-31 mode.
+ jesdcore.set_pattern_gen(mode='PRBS-31')
+ # Monitor each receive lane at DAC.
+ for lane_num in range(0, 4):
+ self.dac.test_mode(mode='PRBS-31', lane=lane_num) # Enable PRBS test mode.
+ number_of_failures = 0
+ for _ in range(0, POLLS_PER_GT):
+ time.sleep(WAIT_TIME_PER_POLL)
+ alarm_pin_dac = self.rh_class.cpld.get_dac_alarm()
+ if alarm_pin_dac:
+ number_of_failures += 1
+ results.append(number_of_failures)
+ if number_of_failures > 0:
+ self.log.error("PRBS-31 test for DAC lane {0} failed {1}/{2}!"
+ .format(lane_num, number_of_failures, POLLS_PER_GT))
+ else:
+ self.log.info("PRBS-31 test for DAC lane {0} passed!"
+ .format(lane_num))
+ self.dac.test_mode(mode='OFF', lane=lane_num) # Disable PRBS test mode.
+ # Disable TX pattern generator at FPGA
+ jesdcore.set_pattern_gen(mode='OFF')
+ return results
+ #
+ WAIT_TIME_PER_POLL = 0.001 # in seconds.
+ POLLS_PER_GT = 100
+ # Create the CSV file.
+ f = open('tx_prbs_sweep.csv', 'w')
+ f.write("Swing,Precursor,Postcursor,Polls,Failures 0,Failures 1,Failures 2,Failures 3\n")
+ # Default TX PHY settings.
+ tx_phy_settings = {"tx_driver_swing": 0b1111, # See UG476, TXDIFFCTRL
+ "tx_precursor" : 0b00000, # See UG476, TXPRECURSOR
+ "tx_postcursor" : 0b00000} # See UG476, TXPOSTCURSOR
+ # Define sweep ranges.
+ DEFAULT_SWING_RANGE = {'start': 0b0000, 'stop': 0b1111 + 0b1, 'step': 1}
+ DEFAULT_CURSOR_RANGE = {'start': 0b00000, 'stop': 0b11111 + 0b1, 'step': 2}
+ swing_range = args.get("swing_range", DEFAULT_SWING_RANGE)
+ precursor_range = args.get("precursor_range", DEFAULT_CURSOR_RANGE)
+ postcursor_range = args.get("postcursor_range", DEFAULT_CURSOR_RANGE)
+ # Test the TX margin across multiple PHY settings.
+ for swing in range(swing_range['start'], swing_range['stop'], swing_range['step']):
+ tx_phy_settings["tx_driver_swing"] = swing
+ for precursor in range(precursor_range['start'], precursor_range['stop'], precursor_range['step']):
+ tx_phy_settings["tx_precursor"] = precursor
+ for postcursor in range(postcursor_range['start'], postcursor_range['stop'], postcursor_range['step']):
+ tx_phy_settings["tx_postcursor"] = postcursor
+ results = _test_lanes(**tx_phy_settings)
+ f.write("{},{},{},{},{},{},{},{}\n".format(
+ tx_phy_settings["tx_driver_swing"],
+ tx_phy_settings["tx_precursor"],
+ tx_phy_settings["tx_postcursor"],
+ POLLS_PER_GT, results[0], results[1], results[2], results[3]))
+ # Housekeeping...
+ f.close()
diff --git a/mpm/python/usrp_mpm/dboard_manager/rh_periphs.py b/mpm/python/usrp_mpm/dboard_manager/rh_periphs.py
new file mode 100644
index 000000000..e9b171c5e
--- /dev/null
+++ b/mpm/python/usrp_mpm/dboard_manager/rh_periphs.py
@@ -0,0 +1,193 @@
+#
+# Copyright 2018 Ettus Research, a National Instruments Company
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+"""
+Rhodium dboard peripherals (CPLD, port expander, dboard regs)
+"""
+
+
+import time
+from usrp_mpm.sys_utils.sysfs_gpio import SysFSGPIO, GPIOBank
+from usrp_mpm.mpmutils import poll_with_timeout
+
+
+class TCA6408(object):
+ """
+ Abstraction layer for the port/gpio expander
+ """
+ pins = (
+ 'PWR-GOOD-3.6V', #3.6V
+ 'PWR-GOOD-1.1V', #1.1V
+ 'PWR-GOOD-2.0V', #2.0V
+ 'PWR-GOOD-5.4V', #5.4V
+ 'PWR-GOOD-5.5V', #6.8V
+ )
+
+ def __init__(self, i2c_dev):
+ if i2c_dev is None:
+ raise RuntimeError("Need to specify i2c device to use the TCA6408")
+ self._gpios = SysFSGPIO({'label': 'tca6408'}, 0x3F, 0x00, 0x00, i2c_dev)
+
+ def set(self, name, value=None):
+ """
+ Assert a pin by name
+ """
+ assert name in self.pins
+ self._gpios.set(self.pins.index(name), value=value)
+
+ def reset(self, name):
+ """
+ Deassert a pin by name
+ """
+ self.set(name, value=0)
+
+ def get(self, name):
+ """
+ Read back a pin by name
+ """
+ assert name in self.pins
+ return self._gpios.get(self.pins.index(name))
+
+
+class FPGAtoDbGPIO(GPIOBank):
+ """
+ Abstraction layer for the FPGA to Daughterboard GPIO
+ """
+ EMIO_BASE = 54+8
+ DB_POWER_ENABLE = 0
+ RF_POWER_ENABLE = 1
+
+ def __init__(self, slot_idx):
+ pwr_base = self.EMIO_BASE + 2*slot_idx
+ GPIOBank.__init__(
+ self,
+ {'label': 'zynq_gpio'},
+ pwr_base,
+ 0x3, # use_mask
+ 0x3, # ddr
+ )
+
+
+class RhCPLD(object):
+ """
+ Control class for the CPLD.
+ """
+
+ REG_SIGNATURE = 0x0000
+ REG_MINOR_REVISION = 0x0001
+ REG_MAJOR_REVISION = 0x0002
+ REG_BUILD_CODE_LSB = 0x0003
+ REG_BUILD_CODE_MSB = 0x0004
+ REG_SCRATCH = 0x0005
+ REG_GAIN_TABLE_SEL = 0x0006
+ REG_DAC_ALARM = 0x0007
+
+ CPLD_SIGNATURE = 0x0045
+ CPLD_MAJOR_REV = 4
+ CPLD_MINOR_REV = 0
+
+ def __init__(self, regs, log):
+ """
+ Initialize communication with the Rh CPLD
+ """
+ self.log = log.getChild("CPLD")
+ self.log.debug("Initializing CPLD...")
+ self.cpld_regs = regs
+ self.poke16 = self.cpld_regs.peek16
+ self.peek16 = self.cpld_regs.peek16
+ # According to the datasheet, The CPLD's internal configuration time can take
+ # anywhere from 0.6 to 3.4 ms. Until then, any ifc accesses will be invalid,
+ # We will blind wait 5 ms and check the signature register to verify operation
+ time.sleep(0.005)
+ signature = self.peek16(self.REG_SIGNATURE)
+ if self.CPLD_SIGNATURE != signature:
+ self.log.error("CPLD Signature Mismatch! " \
+ "Expected: 0x{:04X} Got: 0x{:04X}".format(self.CPLD_SIGNATURE, signature))
+ raise RuntimeError("CPLD Signature Check Failed! "
+ "Incorrect signature readback.")
+ minor_rev = self.peek16(self.REG_MINOR_REVISION)
+ major_rev = self.peek16(self.REG_MAJOR_REVISION)
+ if major_rev != self.CPLD_MAJOR_REV:
+ self.log.error(
+ "CPLD Major Revision check mismatch! Expected: %d Got: %d",
+ self.CPLD_MAJOR_REV,
+ major_rev
+ )
+ raise RuntimeError("CPLD Revision Check Failed! MPM is not "
+ "compatible with the loaded CPLD image.")
+ date_code = self.peek16(self.REG_BUILD_CODE_LSB) | \
+ (self.peek16(self.REG_BUILD_CODE_MSB) << 16)
+ self.log.debug(
+ "CPLD Signature: 0x{:04X} "
+ "Revision: {}.{} "
+ "Date code: 0x{:08X}"
+ .format(signature, major_rev, minor_rev, date_code))
+
+ def get_dac_alarm(self):
+ """
+ This function polls and returns the DAC's ALARM signal connected to the CPLD.
+ """
+ return (self.peek16(self.REG_DAC_ALARM) & 0x0001)
+
+ # TODO: add more control/status functionality to this class?
+
+
+class DboardClockControl(object):
+ """
+ Control the FPGA MMCM for Radio Clock control.
+ """
+ # Clocking Register address constants
+ RADIO_CLK_MMCM = 0x0020
+ PHASE_SHIFT_CONTROL = 0x0024
+ RADIO_CLK_ENABLES = 0x0028
+ MGT_REF_CLK_STATUS = 0x0030
+
+ def __init__(self, regs, log):
+ self.log = log
+ self.regs = regs
+ self.poke32 = self.regs.poke32
+ self.peek32 = self.regs.peek32
+
+ def enable_outputs(self, enable=True):
+ """
+ Enables or disables the MMCM outputs.
+ """
+ if enable:
+ self.poke32(self.RADIO_CLK_ENABLES, 0x011)
+ else:
+ self.poke32(self.RADIO_CLK_ENABLES, 0x000)
+
+ def reset_mmcm(self):
+ """
+ Uninitialize and reset the MMCM
+ """
+ self.log.trace("Disabling all Radio Clocks, then resetting MMCM...")
+ self.enable_outputs(False)
+ self.poke32(self.RADIO_CLK_MMCM, 0x1)
+
+ def enable_mmcm(self):
+ """
+ Unreset MMCM and poll lock indicators
+
+ If MMCM is not locked after unreset, an exception is thrown.
+ """
+ self.log.trace("Un-resetting MMCM...")
+ self.poke32(self.RADIO_CLK_MMCM, 0x2)
+ if not poll_with_timeout(
+ lambda: bool(self.peek32(self.RADIO_CLK_MMCM) & 0x10),
+ 500,
+ 10,
+ ):
+ self.log.error("MMCM not locked!")
+ raise RuntimeError("MMCM not locked!")
+ self.log.trace("MMCM locked. Enabling output MMCM clocks...")
+ self.enable_outputs(True)
+
+ def check_refclk(self):
+ """
+ Not technically a clocking reg, but related.
+ """
+ return bool(self.peek32(self.MGT_REF_CLK_STATUS) & 0x1)
+
diff --git a/mpm/python/usrp_mpm/dboard_manager/rhodium.py b/mpm/python/usrp_mpm/dboard_manager/rhodium.py
new file mode 100644
index 000000000..81ca221a7
--- /dev/null
+++ b/mpm/python/usrp_mpm/dboard_manager/rhodium.py
@@ -0,0 +1,573 @@
+#
+# Copyright 2018 Ettus Research, a National Instruments Company
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+"""
+Rhodium dboard implementation module
+"""
+
+from __future__ import print_function
+import os
+import threading
+from six import iterkeys, iteritems
+from usrp_mpm import lib # Pulls in everything from C++-land
+from usrp_mpm.dboard_manager import DboardManagerBase
+from usrp_mpm.dboard_manager.rh_periphs import TCA6408, FPGAtoDbGPIO
+from usrp_mpm.dboard_manager.rh_init import RhodiumInitManager
+from usrp_mpm.dboard_manager.rh_periphs import RhCPLD
+from usrp_mpm.dboard_manager.rh_periphs import DboardClockControl
+from usrp_mpm.cores import nijesdcore
+from usrp_mpm.dboard_manager.adc_rh import AD9695Rh
+from usrp_mpm.dboard_manager.dac_rh import DAC37J82Rh
+from usrp_mpm.mpmlog import get_logger
+from usrp_mpm.sys_utils.uio import UIO
+from usrp_mpm.sys_utils.udev import get_eeprom_paths
+from usrp_mpm.bfrfs import BufferFS
+
+
+###############################################################################
+# SPI Helpers
+###############################################################################
+
+def create_spidev_iface_lmk(dev_node):
+ """
+ Create a regs iface from a spidev node
+ """
+ return lib.spi.make_spidev_regs_iface(
+ dev_node,
+ 1000000, # Speed (Hz)
+ 0, # SPI mode
+ 8, # Addr shift
+ 0, # Data shift
+ 1<<23, # Read flag
+ 0 # Write flag
+ )
+
+def create_spidev_iface_cpld(dev_node):
+ """
+ Create a regs iface from a spidev node (CPLD register protocol)
+ """
+ return lib.spi.make_spidev_regs_iface(
+ dev_node,
+ 1000000, # Speed (Hz)
+ 0, # SPI mode
+ 17, # Addr shift
+ 0, # Data shift
+ 1<<16, # Read flag
+ 0 # Write flag
+ )
+
+def create_spidev_iface_cpld_gain_loader(dev_node):
+ """
+ Create a regs iface from a spidev node (CPLD gain table protocol)
+ """
+ return lib.spi.make_spidev_regs_iface(
+ dev_node,
+ 1000000, # Speed (Hz)
+ 0, # SPI mode
+ 16, # Addr shift
+ 4, # Data shift
+ 0, # Read flag
+ 1<<3 # Write flag
+ )
+
+def create_spidev_iface_phasedac(dev_node):
+ """
+ Create a regs iface from a spidev node (AD5683)
+
+ The data shift for the SPI interface is defined based on the command
+ operation defined in the AD5683 datasheet.
+ Each SPI transaction is 24-bit: [23:20] -> command; [19:0] -> data
+ The 4 LSBs are all don't cares (Xs), regardless of the DAC's resolution.
+ Therefore, to simplify DAC writes, we compensate for all the don't care
+ bits with the data shift parameter here (4), thus 16-bit data field.
+ Special care must be taken when writing to the control register,
+ since the 6-bit payload is placed in [19:14] of the SPI transaction,
+ which is equivalent to bits [15:10] of our 16-bit data field.
+ For futher details, please refer to the AD5683's datasheet.
+ """
+ return lib.spi.make_spidev_regs_iface(
+ str(dev_node),
+ 1000000, # Speed (Hz)
+ 1, # SPI mode
+ 20, # Addr shift
+ 4, # Data shift
+ 0, # Read flag (phase DAC is write-only)
+ 0, # Write flag
+ )
+
+def create_spidev_iface_adc(dev_node):
+ """
+ Create a regs iface from a spidev node (AD9695)
+ """
+ return lib.spi.make_spidev_regs_iface(
+ str(dev_node),
+ 1000000, # Speed (Hz)
+ 0, # SPI mode
+ 8, # Addr shift
+ 0, # Data shift
+ 1<<23, # Read flag
+ 0, # Write flag
+ )
+
+def create_spidev_iface_dac(dev_node):
+ """
+ Create a regs iface from a spidev node (DAC37J82)
+ """
+ return lib.spi.make_spidev_regs_iface(
+ str(dev_node),
+ 1000000, # Speed (Hz)
+ 0, # SPI mode
+ 16, # Addr shift
+ 0, # Data shift
+ 1<<23, # Read flag
+ 0, # Write flag
+ )
+
+
+###############################################################################
+# Main dboard control class
+###############################################################################
+
+class Rhodium(DboardManagerBase):
+ """
+ Holds all dboard specific information and methods of the Rhodium dboard
+ """
+ #########################################################################
+ # Overridables
+ #
+ # See DboardManagerBase for documentation on these fields
+ #########################################################################
+ pids = [0x152]
+ #file system path to i2c-adapter/mux
+ base_i2c_adapter = '/sys/class/i2c-adapter'
+ # Maps the chipselects to the corresponding devices:
+ spi_chipselect = {
+ "cpld" : 0,
+ "cpld_gain_loader" : 0,
+ "lmk" : 1,
+ "phase_dac" : 2,
+ "adc" : 3,
+ "dac" : 4}
+ ### End of overridables #################################################
+ # Class-specific, but constant settings:
+ spi_factories = {
+ "cpld": create_spidev_iface_cpld,
+ "cpld_gain_loader": create_spidev_iface_cpld_gain_loader,
+ "lmk": create_spidev_iface_lmk,
+ "phase_dac": create_spidev_iface_phasedac,
+ "adc": create_spidev_iface_adc,
+ "dac": create_spidev_iface_dac
+ }
+ # Map I2C channel to slot index
+ i2c_chan_map = {0: 'i2c-9', 1: 'i2c-10'}
+ user_eeprom = {
+ 2: { # RevC
+ 'label': "e0004000.i2c",
+ 'offset': 1024,
+ 'max_size': 32786 - 1024,
+ 'alignment': 1024,
+ },
+ }
+ default_master_clock_rate = 245.76e6
+ default_time_source = 'internal'
+ default_current_jesd_rate = 4915.2e6
+
+ def __init__(self, slot_idx, **kwargs):
+ super(Rhodium, self).__init__(slot_idx, **kwargs)
+ self.log = get_logger("Rhodium-{}".format(slot_idx))
+ self.log.trace("Initializing Rhodium daughterboard, slot index %d",
+ self.slot_idx)
+ self.rev = int(self.device_info['rev'])
+ self.log.trace("This is a rev: {}".format(chr(65 + self.rev)))
+ # This is a default ref clock freq, it must be updated before init() is
+ # called!
+ self.ref_clock_freq = None
+ # These will get updated during init()
+ self.master_clock_rate = None
+ self.sampling_clock_rate = None
+ self.current_jesd_rate = None
+ # Predeclare some attributes to make linter happy:
+ self.lmk = None
+ self._port_expander = None
+ self.cpld = None
+ # If _init_args is None, it means that init() hasn't yet been called.
+ self._init_args = None
+ # Now initialize all peripherals. If that doesn't work, put this class
+ # into a non-functional state (but don't crash, or we can't talk to it
+ # any more):
+ try:
+ self._init_periphs()
+ self._periphs_initialized = True
+ except Exception as ex:
+ self.log.error("Failed to initialize peripherals: %s",
+ str(ex))
+ self._periphs_initialized = False
+
+
+ def _init_periphs(self):
+ """
+ Initialize power and peripherals that don't need user-settings
+ """
+ def _get_i2c_dev():
+ " Return the I2C path for this daughterboard "
+ import pyudev
+ context = pyudev.Context()
+ i2c_dev_path = os.path.join(
+ self.base_i2c_adapter,
+ self.i2c_chan_map[self.slot_idx]
+ )
+ return pyudev.Devices.from_sys_path(context, i2c_dev_path)
+ def _init_spi_devices():
+ " Returns abstraction layers to all the SPI devices "
+ self.log.trace("Loading SPI interfaces...")
+ return {
+ key: self.spi_factories[key](self._spi_nodes[key])
+ for key in self._spi_nodes
+ }
+ def _init_dboard_regs():
+ " Create a UIO object to talk to dboard regs "
+ self.log.trace("Getting UIO to talk to dboard regs...")
+ return UIO(
+ label="dboard-regs-{}".format(self.slot_idx),
+ read_only=False
+ )
+ self._port_expander = TCA6408(_get_i2c_dev())
+ self._daughterboard_gpio = FPGAtoDbGPIO(self.slot_idx)
+ self.log.debug("Turning on Module and RF power supplies")
+ self._power_on()
+ self._spi_ifaces = _init_spi_devices()
+ self.log.debug("Loaded SPI interfaces!")
+ self.cpld = RhCPLD(self._spi_ifaces['cpld'], self.log)
+ self.log.debug("Loaded CPLD interfaces!")
+ self.radio_regs = _init_dboard_regs()
+ self.radio_regs._open()
+ # Create DAC interface (analog output is disabled).
+ self.log.trace("Creating DAC control object...")
+ self.dac = DAC37J82Rh(self.slot_idx, self._spi_ifaces['dac'], self.log)
+ # Create ADC interface (JESD204B link is powered down).
+ self.log.trace("Creating ADC control object...")
+ self.adc = AD9695Rh(self.slot_idx, self._spi_ifaces['adc'], self.log)
+ self.log.info("Succesfully loaded all peripherals!")
+
+ def _power_on(self):
+ " Turn on power to daughterboard "
+ self.log.trace("Powering on slot_idx={}...".format(self.slot_idx))
+ self._daughterboard_gpio.set(FPGAtoDbGPIO.DB_POWER_ENABLE, 1)
+ self._daughterboard_gpio.set(FPGAtoDbGPIO.RF_POWER_ENABLE, 1)
+ # Check each power good signal
+
+ def _power_off(self):
+ " Turn off power to daughterboard "
+ self.log.trace("Powering off slot_idx={}...".format(self.slot_idx))
+ self._daughterboard_gpio.set(FPGAtoDbGPIO.DB_POWER_ENABLE, 0)
+ self._daughterboard_gpio.set(FPGAtoDbGPIO.RF_POWER_ENABLE, 0)
+
+ def _init_user_eeprom(self, eeprom_info):
+ """
+ Reads out user-data EEPROM, and intializes a BufferFS object from that.
+ """
+ self.log.trace("Initializing EEPROM user data...")
+ eeprom_paths = get_eeprom_paths(eeprom_info.get('label'))
+ self.log.trace("Found the following EEPROM paths: `{}'".format(
+ eeprom_paths))
+ eeprom_path = eeprom_paths[self.slot_idx]
+ self.log.trace("Selected EEPROM path: `{}'".format(eeprom_path))
+ user_eeprom_offset = eeprom_info.get('offset', 0)
+ self.log.trace("Selected EEPROM offset: %d", user_eeprom_offset)
+ user_eeprom_data = open(eeprom_path, 'rb').read()[user_eeprom_offset:]
+ self.log.trace("Total EEPROM size is: %d bytes", len(user_eeprom_data))
+ # FIXME verify EEPROM sectors
+ return BufferFS(
+ user_eeprom_data,
+ max_size=eeprom_info.get('max_size'),
+ alignment=eeprom_info.get('alignment', 1024),
+ log=self.log
+ ), eeprom_path
+
+ def init(self, args):
+ """
+ Execute necessary init dance to bring up dboard
+ """
+ # Sanity checks and input validation:
+ self.log.info("init() called with args `{}'".format(
+ ",".join(['{}={}'.format(x, args[x]) for x in args])
+ ))
+ if not self._periphs_initialized:
+ error_msg = "Cannot run init(), peripherals are not initialized!"
+ self.log.error(error_msg)
+ raise RuntimeError(error_msg)
+ # Check if ref clock freq changed (would require a full init)
+ ref_clk_freq_changed = False
+ if 'ref_clk_freq' in args:
+ new_ref_clock_freq = float(args['ref_clk_freq'])
+ assert new_ref_clock_freq in (10e6, 20e6, 25e6)
+ if new_ref_clock_freq != self.ref_clock_freq:
+ self.ref_clock_freq = new_ref_clock_freq
+ ref_clk_freq_changed = True
+ self.log.debug(
+ "Updating reference clock frequency to {:.02f} MHz!"
+ .format(self.ref_clock_freq / 1e6)
+ )
+ assert self.ref_clock_freq is not None
+ # Check if master clock freq changed (would require a full init)
+ new_master_clock_rate = \
+ float(args.get('master_clock_rate', self.default_master_clock_rate))
+ assert new_master_clock_rate in (200e6, 245.76e6, 250e6), \
+ "Invalid master clock rate: {:.02f} MHz".format(new_master_clock_rate / 1e6)
+ master_clock_rate_changed = new_master_clock_rate != self.master_clock_rate
+ if master_clock_rate_changed:
+ self.master_clock_rate = new_master_clock_rate
+ self.log.debug("Updating master clock rate to {:.02f} MHz!".format(
+ self.master_clock_rate / 1e6
+ ))
+ # From the host's perspective (i.e. UHD), master_clock_rate is thought as
+ # the data rate that the radio NoC block works on (200/245.76/250 MSPS).
+ # For Rhodium, that rate is different from the RF sampling rate = JESD rate
+ # (400/491.52/500 MHz). The FPGA has fixed half-band filters that decimate
+ # and interpolate between the radio block and the JESD core.
+ # Therefore, the board configuration through MPM relies on the sampling freq.,
+ # so a sampling_clock_rate value is internally set based on the master_clock_rate
+ # parameter given by the host.
+ self.sampling_clock_rate = 2 * self.master_clock_rate
+ self.log.trace("Updating sampling clock rate to {:.02f} MHz!".format(
+ self.sampling_clock_rate / 1e6
+ ))
+ # Track if we're able to do a "fast reinit", which means there were no
+ # major changes and can skip all slow initialization steps.
+ fast_reinit = \
+ not bool(args.get("force_reinit", False)) \
+ and not master_clock_rate_changed \
+ and not ref_clk_freq_changed
+ if fast_reinit:
+ self.log.debug("Attempting fast re-init with the following settings: "
+ "master_clock_rate={} MHz ref_clk_freq={} MHz"
+ .format(self.master_clock_rate / 1e6, self.ref_clock_freq / 1e6))
+ init_result = True
+ else:
+ init_result = RhodiumInitManager(self, self._spi_ifaces).init(args)
+ if init_result:
+ self._init_args = args
+ return init_result
+
+ def get_user_eeprom_data(self):
+ """
+ Return a dict of blobs stored in the user data section of the EEPROM.
+ """
+ return {
+ blob_id: self.eeprom_fs.get_blob(blob_id)
+ for blob_id in iterkeys(self.eeprom_fs.entries)
+ }
+
+ def set_user_eeprom_data(self, eeprom_data):
+ """
+ Update the local EEPROM with the data from eeprom_data.
+
+ The actual writing to EEPROM can take some time, and is thus kicked
+ into a background task. Don't call set_user_eeprom_data() quickly in
+ succession. Also, while the background task is running, reading the
+ EEPROM is unavailable and MPM won't be able to reboot until it's
+ completed.
+ However, get_user_eeprom_data() will immediately return the correct
+ data after this method returns.
+ """
+ for blob_id, blob in iteritems(eeprom_data):
+ self.eeprom_fs.set_blob(blob_id, blob)
+ self.log.trace("Writing EEPROM info to `{}'".format(self.eeprom_path))
+ eeprom_offset = self.user_eeprom[self.rev]['offset']
+ def _write_to_eeprom_task(path, offset, data, log):
+ " Writer task: Actually write to file "
+ # Note: This can be sped up by only writing sectors that actually
+ # changed. To do so, this function would need to read out the
+ # current state of the file, do some kind of diff, and then seek()
+ # to the different sectors. When very large blobs are being
+ # written, it doesn't actually help all that much, of course,
+ # because in that case, we'd anyway be changing most of the EEPROM.
+ with open(path, 'r+b') as eeprom_file:
+ log.trace("Seeking forward to `{}'".format(offset))
+ eeprom_file.seek(eeprom_offset)
+ log.trace("Writing a total of {} bytes.".format(
+ len(self.eeprom_fs.buffer)))
+ eeprom_file.write(data)
+ log.trace("EEPROM write complete.")
+ thread_id = "eeprom_writer_task_{}".format(self.slot_idx)
+ if any([x.name == thread_id for x in threading.enumerate()]):
+ # Should this be fatal?
+ self.log.warn("Another EEPROM writer thread is already active!")
+ writer_task = threading.Thread(
+ target=_write_to_eeprom_task,
+ args=(
+ self.eeprom_path,
+ eeprom_offset,
+ self.eeprom_fs.buffer,
+ self.log
+ ),
+ name=thread_id,
+ )
+ writer_task.start()
+ # Now return and let the copy finish on its own. The thread will detach
+ # and MPM this process won't terminate until the thread is complete.
+ # This does not stop anyone from killing this process (and the thread)
+ # while the EEPROM write is happening, though.
+
+
+ ##########################################################################
+ # Clocking control APIs
+ ##########################################################################
+
+ def set_clk_safe_state(self):
+ """
+ Disable all components that could react badly to a sudden change in
+ clocking. After calling this method, all clocks will be off. Calling
+ _reinit() will turn them on again.
+ """
+ if self._init_args is None:
+ # Then we're already in a safe state
+ return
+ # Put the ADC and the DAC in a safe state because they receive a LMK's clock.
+ # The DAC37J82 datasheet only recommends disabling its analog output before
+ # a clock is provided to the chip.
+ self.dac.tx_enable(False)
+ self.adc.power_down_channel(True)
+ # Clear the Sample Clock enables and place the MMCM in reset.
+ db_clk_control = DboardClockControl(self.radio_regs, self.log)
+ db_clk_control.reset_mmcm()
+ # Place the JESD204b core in reset, mainly to reset QPLL/CPLLs.
+ jesdcore = nijesdcore.NIJESDCore(self.radio_regs, self.slot_idx,
+ **RhodiumInitManager.JESD_DEFAULT_ARGS)
+ jesdcore.reset()
+ # The reference clock is handled elsewhere since it is a motherboard-
+ # level clock.
+
+ def _reinit(self, master_clock_rate):
+ """
+ This will re-run init(). We store all the settings in _init_args, so we
+ will bring the device into the same state that it was before, with the
+ exception of frequency and gain. Those need to be re-set by UHD in order
+ not to invalidate the UHD caches.
+ """
+ args = self._init_args
+ args["master_clock_rate"] = master_clock_rate
+ args["ref_clk_freq"] = self.ref_clock_freq
+ # If we add API calls to reset the cals, they need to update
+ # self._init_args
+ self.master_clock_rate = None # <= This will force a re-init
+ self.init(args)
+ # self.master_clock_rate is now OK again
+
+ def set_master_clock_rate(self, rate):
+ """
+ Set the master clock rate to rate. Note this will trigger a
+ re-initialization of the entire clocking, unless rate matches the
+ current master clock rate.
+ """
+ if rate == self.master_clock_rate:
+ self.log.debug(
+ "New master clock rate assignment matches previous assignment. "
+ "Ignoring set_master_clock_rate() command.")
+ return self.master_clock_rate
+ self._reinit(rate)
+ return rate
+
+ def get_master_clock_rate(self):
+ " Return master clock rate (== sampling rate / 2) "
+ return self.master_clock_rate
+
+ def update_ref_clock_freq(self, freq, **kwargs):
+ """
+ Call this function if the frequency of the reference clock changes
+ (the 10, 20, 25 MHz one).
+
+ If this function is called while the device is in an initialized state,
+ it will also re-trigger the initialization sequence.
+
+ No need to set the device in a safe state because (presumably) the user
+ has already switched the clock rate externally. All we need to do now
+ is re-initialize with the new rate.
+ """
+ assert freq in (10e6, 20e6, 25e6), \
+ "Invalid ref clock frequency: {}".format(freq)
+ self.log.trace("Changing ref clock frequency to %f MHz", freq/1e6)
+ self.ref_clock_freq = freq
+ if self._init_args is not None:
+ self._reinit(self.master_clock_rate)
+
+
+ ##########################################################################
+ # Debug
+ ##########################################################################
+
+ def cpld_peek(self, addr):
+ """
+ Debug for accessing the CPLD via the RPC shell.
+ """
+ self.log.trace("CPLD Signature: 0x{:X}".format(self.cpld.peek(0x00)))
+ revision_msb = self.cpld.peek16(0x04)
+ self.log.trace("CPLD Revision: 0x{:X}"
+ .format(self.cpld.peek16(0x03) | (revision_msb << 16)))
+ return self.cpld.peek16(addr)
+
+ def cpld_poke(self, addr, data):
+ """
+ Debug for accessing the CPLD via the RPC shell.
+ """
+ self.log.trace("CPLD Signature: 0x{:X}".format(self.cpld.peek16(0x00)))
+ revision_msb = self.cpld.peek16(0x04)
+ self.log.trace("CPLD Revision: 0x{:X}"
+ .format(self.cpld.peek16(0x03) | (revision_msb << 16)))
+ self.cpld.poke16(addr, data)
+ return self.cpld.peek16(addr)
+
+ def lmk_peek(self, addr):
+ """
+ Debug for accessing the LMK via the RPC shell.
+ """
+ lmk_regs = self._spi_ifaces['lmk']
+ self.log.trace("LMK Chip ID: 0x{:X}".format(lmk_regs.peek8(0x03)))
+ return lmk_regs.peek8(addr)
+
+ def lmk_poke(self, addr, data):
+ """
+ Debug for accessing the LMK via the RPC shell.
+ """
+ lmk_regs = self._spi_ifaces['lmk']
+ self.log.trace("LMK Chip ID: 0x{:X}".format(lmk_regs.peek8(0x03)))
+ lmk_regs.poke8(addr, data)
+ return lmk_regs.peek8(addr)
+
+ def pdac_poke(self, addr, data):
+ """
+ Debug for accessing the Phase DAC via the RPC shell.
+ """
+ pdac_regs = self._spi_ifaces['phase_dac']
+ pdac_regs.poke16(addr, data)
+ return
+
+ def adc_peek(self, addr):
+ """
+ Debug for accessing the ADC via the RPC shell.
+ """
+ adc_regs = self._spi_ifaces['adc']
+ self.log.trace("ADC Chip ID: 0x{:X}".format(adc_regs.peek8(0x04)))
+ return adc_regs.peek8(addr)
+
+ def adc_poke(self, addr, data):
+ """
+ Debug for accessing the ADC via the RPC shell
+ """
+ adc_regs = self._spi_ifaces['adc']
+ self.log.trace("ADC Chip ID: 0x{:X}".format(adc_regs.peek8(0x04)))
+ adc_regs.poke8(addr, data)
+ return adc_regs.peek8(addr)
+
+ def dump_jesd_core(self):
+ """
+ Debug for reading out all JESD core registers via RPC shell
+ """
+ radio_regs = UIO(label="dboard-regs-{}".format(self.slot_idx))
+ for i in range(0x2000, 0x2110, 0x10):
+ print(("0x%04X " % i), end=' ')
+ for j in range(0, 0x10, 0x4):
+ print(("%08X" % radio_regs.peek32(i + j)), end=' ')
+ print("")
diff --git a/mpm/python/usrp_mpm/periph_manager/n3xx.py b/mpm/python/usrp_mpm/periph_manager/n3xx.py
index 33966a9cb..79dddd898 100644
--- a/mpm/python/usrp_mpm/periph_manager/n3xx.py
+++ b/mpm/python/usrp_mpm/periph_manager/n3xx.py
@@ -28,6 +28,7 @@ from usrp_mpm.periph_manager.n3xx_periphs import BackpanelGPIO
from usrp_mpm.periph_manager.n3xx_periphs import MboardRegsControl
from usrp_mpm.dboard_manager.magnesium import Magnesium
from usrp_mpm.dboard_manager.eiscat import EISCAT
+from usrp_mpm.dboard_manager.rhodium import Rhodium
N3XX_DEFAULT_EXT_CLOCK_FREQ = 10e6
N3XX_DEFAULT_CLOCK_SOURCE = 'internal'
@@ -41,6 +42,7 @@ N3XX_MONITOR_THREAD_INTERVAL = 1.0 # seconds
# Import daughterboard PIDs from their respective classes
MG_PID = Magnesium.pids[0]
EISCAT_PID = EISCAT.pids[0]
+RHODIUM_PID = Rhodium.pids[0]
###############################################################################
# Transport managers
@@ -101,7 +103,9 @@ class n3xx(ZynqComponents, PeriphManagerBase):
# still use the n310.bin image.
# We'll leave this here for
# debugging purposes.
- ('n310', (EISCAT_PID, EISCAT_PID)): 'eiscat',
+ ('n310', (EISCAT_PID , EISCAT_PID )): 'eiscat',
+ ('n310', (RHODIUM_PID, RHODIUM_PID)): 'n320',
+ ('n310', (RHODIUM_PID, )): 'n320',
}
#########################################################################