aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--host/include/uhd/rfnoc/blocks/radio_rhodium.xml42
-rw-r--r--host/lib/CMakeLists.txt3
-rw-r--r--host/lib/ic_reg_maps/CMakeLists.txt7
-rw-r--r--host/lib/ic_reg_maps/gen_rhcpld_regs.py93
-rw-r--r--host/lib/usrp/dboard/CMakeLists.txt5
-rw-r--r--host/lib/usrp/dboard/rhodium/CMakeLists.txt18
-rw-r--r--host/lib/usrp/dboard/rhodium/rhodium_bands.cpp135
-rw-r--r--host/lib/usrp/dboard/rhodium/rhodium_constants.hpp69
-rw-r--r--host/lib/usrp/dboard/rhodium/rhodium_cpld_ctrl.cpp474
-rw-r--r--host/lib/usrp/dboard/rhodium/rhodium_cpld_ctrl.hpp344
-rw-r--r--host/lib/usrp/dboard/rhodium/rhodium_radio_ctrl_cpld.cpp374
-rw-r--r--host/lib/usrp/dboard/rhodium/rhodium_radio_ctrl_impl.cpp437
-rw-r--r--host/lib/usrp/dboard/rhodium/rhodium_radio_ctrl_impl.hpp345
-rw-r--r--host/lib/usrp/dboard/rhodium/rhodium_radio_ctrl_init.cpp710
-rw-r--r--host/lib/usrp/dboard/rhodium/rhodium_radio_ctrl_lo.cpp569
-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
26 files changed, 6147 insertions, 4 deletions
diff --git a/host/include/uhd/rfnoc/blocks/radio_rhodium.xml b/host/include/uhd/rfnoc/blocks/radio_rhodium.xml
new file mode 100644
index 000000000..a14acea91
--- /dev/null
+++ b/host/include/uhd/rfnoc/blocks/radio_rhodium.xml
@@ -0,0 +1,42 @@
+<!--This defines the Radio (Rhodium) NoC-Block.-->
+<nocblock>
+ <name>Radio (Rhodium)</name>
+ <blockname>Radio</blockname>
+ <key>RhodiumRadio</key>
+ <!--There can be several of these:-->
+ <ids>
+ <id revision="0">12AD100000000320</id>
+ </ids>
+ <!-- Registers -->
+ <registers>
+ </registers>
+ <!-- Args -->
+ <args>
+ <arg>
+ <name>spp</name>
+ <type>int</type>
+ <value>364</value>
+ </arg>
+ <arg>
+ <name>spur_dodging</name>
+ <type>string</type>
+ <value>disabled</value>
+ </arg>
+ <arg>
+ <name>spur_dodging_threshold</name>
+ <type>double</type>
+ <value>2e6</value>
+ </arg>
+ </args>
+ <ports>
+ <sink>
+ <name>in0</name>
+ <type>sc16</type>
+ </sink>
+ <source>
+ <name>out0</name>
+ <type>sc16</type>
+ </source>
+ </ports>
+</nocblock>
+
diff --git a/host/lib/CMakeLists.txt b/host/lib/CMakeLists.txt
index 99598570e..f5539ef6f 100644
--- a/host/lib/CMakeLists.txt
+++ b/host/lib/CMakeLists.txt
@@ -2,7 +2,7 @@
# Copyright 2010-2015 Ettus Research LLC
# Copyright 2018 Ettus Research, a National Instruments Company
#
-# SPDX-License-Identifier: GPL-3.0
+# SPDX-License-Identifier: GPL-3.0-or-later
#
########################################################################
@@ -76,6 +76,7 @@ LIBUHD_REGISTER_COMPONENT("X300" ENABLE_X300 ON "ENABLE_LIBUHD" OFF OFF)
LIBUHD_REGISTER_COMPONENT("N230" ENABLE_N230 ON "ENABLE_LIBUHD" OFF OFF)
LIBUHD_REGISTER_COMPONENT("MPMD" ENABLE_MPMD ON "ENABLE_LIBUHD" OFF OFF)
LIBUHD_REGISTER_COMPONENT("N300" ENABLE_N300 ON "ENABLE_LIBUHD;ENABLE_MPMD" OFF OFF)
+# LIBUHD_REGISTER_COMPONENT("N320" ENABLE_N320 ON "ENABLE_LIBUHD;ENABLE_MPMD" OFF OFF)
LIBUHD_REGISTER_COMPONENT("E320" ENABLE_E320 ON "ENABLE_LIBUHD;ENABLE_MPMD" OFF OFF)
LIBUHD_REGISTER_COMPONENT("OctoClock" ENABLE_OCTOCLOCK ON "ENABLE_LIBUHD" OFF OFF)
LIBUHD_REGISTER_COMPONENT("DPDK" ENABLE_DPDK ON "ENABLE_MPMD;DPDK_FOUND" OFF OFF)
diff --git a/host/lib/ic_reg_maps/CMakeLists.txt b/host/lib/ic_reg_maps/CMakeLists.txt
index de0e0660e..3f06d9419 100644
--- a/host/lib/ic_reg_maps/CMakeLists.txt
+++ b/host/lib/ic_reg_maps/CMakeLists.txt
@@ -2,7 +2,7 @@
# Copyright 2010-2013 Ettus Research LLC
# Copyright 2018 Ettus Research, a National Instruments Company
#
-# SPDX-License-Identifier: GPL-3.0
+# SPDX-License-Identifier: GPL-3.0-or-later
#
########################################################################
@@ -137,4 +137,9 @@ LIBUHD_PYTHON_GEN_SOURCE(
${CMAKE_CURRENT_BINARY_DIR}/magnesium_cpld_regs.hpp
)
+LIBUHD_PYTHON_GEN_SOURCE(
+ ${CMAKE_CURRENT_SOURCE_DIR}/gen_rhcpld_regs.py
+ ${CMAKE_CURRENT_BINARY_DIR}/rhodium_cpld_regs.hpp
+)
+
SET(LIBUHD_PYTHON_GEN_SOURCE_DEPS)
diff --git a/host/lib/ic_reg_maps/gen_rhcpld_regs.py b/host/lib/ic_reg_maps/gen_rhcpld_regs.py
new file mode 100644
index 000000000..1ce88fdcb
--- /dev/null
+++ b/host/lib/ic_reg_maps/gen_rhcpld_regs.py
@@ -0,0 +1,93 @@
+#!/usr/bin/env python
+#
+# Copyright 2018 Ettus Research, a National Instruments Company
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+"""
+Register map for the Rhodium CPLD. This is controlled via SPI.
+"""
+
+########################################################################
+# Template for raw text data describing registers
+# name addr[bit range inclusive] default optional enums
+########################################################################
+REGS_TMPL="""\
+########################################################################
+## address 5 Scratch
+########################################################################
+scratch 5[0:15] 0x0
+########################################################################
+## address 6 Rx Band Select
+########################################################################
+reg6_reserved0 6[0] 0x0
+rx_sw1 6[1:2] 1 cal_loopback, rx2, isolation, txrx
+rx_sw2_sw7 6[3] 0 lowband, highband
+rx_sw3 6[4:5] 0 rx_sw4, filter7, filter6, filter5
+rx_sw4_sw5 6[6:9] 1 filter2=1, filter1=2, filter4=4, filter3=8
+rx_sw6 6[10:11] 3 filter5, filter6, filter7, rx_sw5
+rx_gain_tbl_sel 6[12] 0 lowband, highband
+reg6_reserved1 6[13:15] 0x0
+########################################################################
+## address 7 Tx Band Select
+########################################################################
+reg7_reserved0 7[0:1] 0x0
+tx_sw1 7[2:3] 3 lowband_if, tx_sw2, cal_loopback, isolation
+tx_sw2 7[4:5] 0 tx_sw3, filter7, filter6, filter5
+tx_sw3_sw4 7[6:9] 1 filter2=1, filter1=2, filter4=4, filter3=8
+tx_sw5 7[10:11] 0 filter5, filter6, filter7, tx_sw4
+tx_gain_tbl_sel 7[12] 0 lowband, highband
+reg7_reserved1 7[13:15] 0x0
+########################################################################
+## address 8 Misc Switches
+########################################################################
+reg8_reserved0 8[0:2] 0x0
+cal_iso_sw 8[3] 0 isolation, cal_loopback
+tx_hb_lb_sel 8[4] 0 lowband, highband
+reg8_reserved1 8[5] 0
+tx_lo_input_sel 8[6] 0 internal, external
+rx_hb_lb_sel 8[7] 0 lowband, highband
+reg8_reserved2 8[8] 0
+rx_lo_input_sel 8[9] 1 external, internal
+rx_demod_adj 8[10:11] 0 res_open=0, res_200_ohm=1, res_1500_ohm=2
+tx_lo_filter_sel 8[12:13] 3 0_9ghz_lpf, 5_85ghz_lpf, 2_25ghz_lpf, isolation
+rx_lo_filter_sel 8[14:15] 3 0_9ghz_lpf, 5_85ghz_lpf, 2_25ghz_lpf, isolation
+"""
+
+########################################################################
+# Template for methods in the body of the struct
+########################################################################
+BODY_TMPL="""\
+uint32_t get_reg(uint8_t addr){
+ uint32_t reg = 0;
+ switch(addr){
+ % for addr in range(5, 14+1):
+ case ${addr}:
+ % for reg in filter(lambda r: r.get_addr() == addr, regs):
+ reg |= (uint32_t(${reg.get_name()}) & ${reg.get_mask()}) << ${reg.get_shift()};
+ % endfor
+ break;
+ % endfor
+ }
+ return reg;
+}
+
+std::set<size_t> get_all_addrs()
+{
+ std::set<size_t> addrs;
+ % for reg in regs:
+ // Hopefully, compilers will optimize out this mess...
+ addrs.insert(${reg.get_addr()});
+ % endfor
+ return addrs;
+}
+"""
+
+if __name__ == '__main__':
+ import common; common.generate(
+ name='rhodium_cpld_regs',
+ regs_tmpl=REGS_TMPL,
+ body_tmpl=BODY_TMPL,
+ file=__file__,
+ )
+
diff --git a/host/lib/usrp/dboard/CMakeLists.txt b/host/lib/usrp/dboard/CMakeLists.txt
index 8ebe8e642..25e1bd2b9 100644
--- a/host/lib/usrp/dboard/CMakeLists.txt
+++ b/host/lib/usrp/dboard/CMakeLists.txt
@@ -2,7 +2,7 @@
# Copyright 2010-2011 Ettus Research LLC
# Copyright 2018 Ettus Research, a National Instruments Company
#
-# SPDX-License-Identifier: GPL-3.0
+# SPDX-License-Identifier: GPL-3.0-or-later
#
########################################################################
@@ -49,6 +49,9 @@ ENDIF(ENABLE_X300)
IF(ENABLE_N300)
INCLUDE_SUBDIRECTORY(magnesium)
ENDIF(ENABLE_N300)
+IF(ENABLE_N320)
+ INCLUDE_SUBDIRECTORY(rhodium)
+ENDIF(ENABLE_N320)
IF(ENABLE_MPMD AND ENABLE_EISCAT)
INCLUDE_SUBDIRECTORY(eiscat)
ENDIF(ENABLE_MPMD AND ENABLE_EISCAT)
diff --git a/host/lib/usrp/dboard/rhodium/CMakeLists.txt b/host/lib/usrp/dboard/rhodium/CMakeLists.txt
new file mode 100644
index 000000000..617e4af58
--- /dev/null
+++ b/host/lib/usrp/dboard/rhodium/CMakeLists.txt
@@ -0,0 +1,18 @@
+#
+# Copyright 2018 Ettus Research, a National Instruments Company
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+
+IF(ENABLE_MPMD)
+ LIST(APPEND RHODIUM_SOURCES
+ ${CMAKE_CURRENT_SOURCE_DIR}/rhodium_radio_ctrl_impl.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/rhodium_radio_ctrl_init.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/rhodium_radio_ctrl_cpld.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/rhodium_radio_ctrl_lo.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/rhodium_bands.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/rhodium_cpld_ctrl.cpp
+ )
+ LIBUHD_APPEND_SOURCES(${RHODIUM_SOURCES})
+ENDIF(ENABLE_MPMD)
+
diff --git a/host/lib/usrp/dboard/rhodium/rhodium_bands.cpp b/host/lib/usrp/dboard/rhodium/rhodium_bands.cpp
new file mode 100644
index 000000000..ffa206195
--- /dev/null
+++ b/host/lib/usrp/dboard/rhodium/rhodium_bands.cpp
@@ -0,0 +1,135 @@
+//
+// Copyright 2018 Ettus Research, a National Instruments Company
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+#include "rhodium_radio_ctrl_impl.hpp"
+#include "rhodium_constants.hpp"
+#include <uhd/utils/math.hpp>
+
+using namespace uhd;
+using namespace uhd::usrp;
+using namespace uhd::rfnoc;
+using namespace uhd::math::fp_compare;
+
+namespace {
+ constexpr double FREQ_COMPARE_EPSILON = 1e-5;
+
+ /* Note on the RX filter bank:
+ *
+ * The RX path has 8 bands, which we call BAND0 through BAND7. BAND0 is the
+ * lowest frequency band. BAND7 is the highest frequency band.
+ *
+ * The following constants define lower cutoff frequencies for each band.
+ * RHODIUM_RX_BAND1_MIN_FREQ is the cutover frequency for switching from
+ * BAND0 to BAND1, and so on.
+ *
+ * Bands 1-7 have both high- and low-pass filters (effectively band
+ * passes). Band 0 has only low-pass filters. Frequencies have been
+ * chosen to allow as much of the full bandwidth through unattenuated.
+ *
+ * Switch selection logic for these bands can be found in
+ * rhodium_radio_ctrl_impl::_update_rx_freq_switches()
+ */
+ constexpr double RHODIUM_RX_BAND0_MIN_FREQ = RHODIUM_MIN_FREQ;
+ constexpr double RHODIUM_RX_BAND1_MIN_FREQ = 450e6;
+ constexpr double RHODIUM_RX_BAND2_MIN_FREQ = 760e6;
+ constexpr double RHODIUM_RX_BAND3_MIN_FREQ = 1100e6;
+ constexpr double RHODIUM_RX_BAND4_MIN_FREQ = 1410e6;
+ constexpr double RHODIUM_RX_BAND5_MIN_FREQ = 2050e6;
+ constexpr double RHODIUM_RX_BAND6_MIN_FREQ = 3000e6;
+ constexpr double RHODIUM_RX_BAND7_MIN_FREQ = 4500e6;
+
+ /* Note on the TX filter bank:
+ *
+ * The TX path has 8 bands, which we call BAND0 through BAND7. BAND0 is the
+ * lowest frequency band. BAND7 is the highest frequency band.
+ *
+ * The following constants define lower cutoff frequencies for each band.
+ * RHODIUM_TX_BAND1_MIN_FREQ is the cutover frequency for switching from
+ * BAND0 to BAND1, and so on.
+ *
+ * All filters on the TX filter bank are low pass filters (no high pass
+ * filters). Frequencies have been chosen to allow as much of the full
+ * bandwidth through unattenuated.
+ *
+ * Switch selection logic for these bands can be found in
+ * rhodium_radio_ctrl_impl::_update_tx_freq_switches()
+ */
+ constexpr double RHODIUM_TX_BAND0_MIN_FREQ = RHODIUM_MIN_FREQ;
+ constexpr double RHODIUM_TX_BAND1_MIN_FREQ = 450e6;
+ constexpr double RHODIUM_TX_BAND2_MIN_FREQ = 650e6;
+ constexpr double RHODIUM_TX_BAND3_MIN_FREQ = 1000e6;
+ constexpr double RHODIUM_TX_BAND4_MIN_FREQ = 1350e6;
+ constexpr double RHODIUM_TX_BAND5_MIN_FREQ = 1900e6;
+ constexpr double RHODIUM_TX_BAND6_MIN_FREQ = 3000e6;
+ constexpr double RHODIUM_TX_BAND7_MIN_FREQ = 4100e6;
+}
+
+rhodium_radio_ctrl_impl::rx_band
+rhodium_radio_ctrl_impl::_map_freq_to_rx_band(const double freq) {
+
+ auto freq_compare = fp_compare_epsilon<double>(freq, RHODIUM_FREQ_COMPARE_EPSILON);
+
+ if (freq_compare < RHODIUM_RX_BAND0_MIN_FREQ) {
+ return rx_band::RX_BAND_INVALID;
+ } else if (freq_compare < RHODIUM_RX_BAND1_MIN_FREQ) {
+ return rx_band::RX_BAND_0;
+ } else if (freq_compare < RHODIUM_RX_BAND2_MIN_FREQ) {
+ return rx_band::RX_BAND_1;
+ } else if (freq_compare < RHODIUM_RX_BAND3_MIN_FREQ) {
+ return rx_band::RX_BAND_2;
+ } else if (freq_compare < RHODIUM_RX_BAND4_MIN_FREQ) {
+ return rx_band::RX_BAND_3;
+ } else if (freq_compare < RHODIUM_RX_BAND5_MIN_FREQ) {
+ return rx_band::RX_BAND_4;
+ } else if (freq_compare < RHODIUM_RX_BAND6_MIN_FREQ) {
+ return rx_band::RX_BAND_5;
+ } else if (freq_compare < RHODIUM_RX_BAND7_MIN_FREQ) {
+ return rx_band::RX_BAND_6;
+ } else if (freq_compare <= RHODIUM_MAX_FREQ) {
+ return rx_band::RX_BAND_7;
+ } else {
+ return rx_band::RX_BAND_INVALID;
+ }
+}
+
+rhodium_radio_ctrl_impl::tx_band
+rhodium_radio_ctrl_impl::_map_freq_to_tx_band(const double freq) {
+
+ auto freq_compare = fp_compare_epsilon<double>(freq, RHODIUM_FREQ_COMPARE_EPSILON);
+
+ if (freq_compare < RHODIUM_TX_BAND0_MIN_FREQ) {
+ return tx_band::TX_BAND_INVALID;
+ } else if (freq_compare < RHODIUM_TX_BAND1_MIN_FREQ) {
+ return tx_band::TX_BAND_0;
+ } else if (freq_compare < RHODIUM_TX_BAND2_MIN_FREQ) {
+ return tx_band::TX_BAND_1;
+ } else if (freq_compare < RHODIUM_TX_BAND3_MIN_FREQ) {
+ return tx_band::TX_BAND_2;
+ } else if (freq_compare < RHODIUM_TX_BAND4_MIN_FREQ) {
+ return tx_band::TX_BAND_3;
+ } else if (freq_compare < RHODIUM_TX_BAND5_MIN_FREQ) {
+ return tx_band::TX_BAND_4;
+ } else if (freq_compare < RHODIUM_TX_BAND6_MIN_FREQ) {
+ return tx_band::TX_BAND_5;
+ } else if (freq_compare < RHODIUM_TX_BAND7_MIN_FREQ) {
+ return tx_band::TX_BAND_6;
+ } else if (freq_compare <= RHODIUM_MAX_FREQ) {
+ return tx_band::TX_BAND_7;
+ } else {
+ return tx_band::TX_BAND_INVALID;
+ }
+}
+
+bool rhodium_radio_ctrl_impl::_is_rx_lowband(const double freq)
+{
+ return _map_freq_to_rx_band(freq) == rx_band::RX_BAND_0;
+}
+
+bool rhodium_radio_ctrl_impl::_is_tx_lowband(const double freq)
+{
+ return _map_freq_to_tx_band(freq) == tx_band::TX_BAND_0;
+}
+
diff --git a/host/lib/usrp/dboard/rhodium/rhodium_constants.hpp b/host/lib/usrp/dboard/rhodium/rhodium_constants.hpp
new file mode 100644
index 000000000..82ed5b4c8
--- /dev/null
+++ b/host/lib/usrp/dboard/rhodium/rhodium_constants.hpp
@@ -0,0 +1,69 @@
+//
+// Copyright 2018 Ettus Research, a National Instruments Company
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+#ifndef INCLUDED_LIBUHD_RHODIUM_CONSTANTS_HPP
+#define INCLUDED_LIBUHD_RHODIUM_CONSTANTS_HPP
+
+#include <vector>
+#include <string>
+#include <cstddef>
+
+static constexpr double RHODIUM_FREQ_COMPARE_EPSILON = 1e-5;
+
+static constexpr double RHODIUM_RADIO_RATE = 122.88e6; // Hz
+static constexpr double RHODIUM_MIN_FREQ = 1e6; // Hz
+static constexpr double RHODIUM_MAX_FREQ = 6e9; // Hz
+
+static constexpr double RHODIUM_LO1_MIN_FREQ = 450e6; // Hz
+static constexpr double RHODIUM_LO1_MAX_FREQ = 6e9; // Hz
+static constexpr double RHODIUM_LO1_REF_FREQ = 122.88e6; // Hz
+
+static constexpr double RHODIUM_LO_0_9_GHZ_LPF_THRESHOLD_FREQ = 0.9e9; // Hz
+static constexpr double RHODIUM_LO_2_25_GHZ_LPF_THRESHOLD_FREQ = 2.3e9; // Hz
+
+static constexpr double RHODIUM_LOWBAND_FREQ = 450e6; // Hz
+static constexpr double RHODIUM_RX_IF_FREQ = 2.44e9; // Hz
+static constexpr double RHODIUM_TX_IF_FREQ = 1.95e9; // Hz
+
+static constexpr double RX_MIN_GAIN = 0.0;
+static constexpr double RX_MAX_GAIN = 60.0;
+static constexpr double RX_GAIN_STEP = 1.0;
+static constexpr double TX_MIN_GAIN = 0.0;
+static constexpr double TX_MAX_GAIN = 60.0;
+static constexpr double TX_GAIN_STEP = 1.0;
+
+static constexpr double LO_MIN_GAIN = 0.0;
+static constexpr double LO_MAX_GAIN = 30.0;
+static constexpr double LO_GAIN_STEP = 1.0;
+
+static constexpr double LO_MIN_POWER = 0.0;
+static constexpr double LO_MAX_POWER = 63.0;
+static constexpr double LO_POWER_STEP = 1.0;
+
+static const std::vector<std::string> RHODIUM_RX_ANTENNAS = {
+ "TX/RX", "RX2", "CAL", "TERM"
+};
+
+static const std::vector<std::string> RHODIUM_TX_ANTENNAS = {
+ "TX/RX", "CAL", "TERM"
+};
+
+static constexpr uint32_t SW10_GPIO_MASK = 0x3;
+
+//! Main LO
+static constexpr char RHODIUM_LO1[] = "lo1";
+//! Low-band LO (for IF conversion)
+static constexpr char RHODIUM_LO2[] = "lowband";
+//! DSA attenuation
+static constexpr char RHODIUM_GAIN[] = "gain_table";
+//! LO DSA attenuation
+static constexpr char RHODIUM_LO_GAIN[] = "dsa";
+//! LO output power
+static constexpr char RHODIUM_LO_POWER[] = "lo";
+
+static constexpr size_t RHODIUM_NUM_CHANS = 1;
+
+#endif /* INCLUDED_LIBUHD_RHODIUM_CONSTANTS_HPP */
diff --git a/host/lib/usrp/dboard/rhodium/rhodium_cpld_ctrl.cpp b/host/lib/usrp/dboard/rhodium/rhodium_cpld_ctrl.cpp
new file mode 100644
index 000000000..8d294bc48
--- /dev/null
+++ b/host/lib/usrp/dboard/rhodium/rhodium_cpld_ctrl.cpp
@@ -0,0 +1,474 @@
+//
+// Copyright 2018 Ettus Research, a National Instruments Company
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+#include "rhodium_cpld_ctrl.hpp"
+#include "rhodium_constants.hpp"
+#include <uhd/utils/math.hpp>
+#include <uhd/utils/log.hpp>
+#include <boost/format.hpp>
+#include <chrono>
+
+using namespace uhd;
+using namespace uhd::math::fp_compare;
+
+namespace {
+ //! Address of the CPLD scratch register
+ constexpr uint8_t CPLD_REGS_SCRATCH = 0x05;
+ //! Address of the CPLD gain table selection register
+ constexpr uint8_t CPLD_REGS_GAIN_TBL_SEL = 0x06;
+
+ constexpr uint32_t MAX_GAIN_INDEX = 60;
+ constexpr uint32_t MAX_LO_GAIN_INDEX = 30;
+
+ //! RX Demodulator Adjustment thresholds
+ constexpr double RX_DEMOD_ADJ_1500OHM_THRESHOLD = 3e9;
+ constexpr double RX_DEMOD_ADJ_200OHM_THRESHOLD = 4.5e9;
+
+ /*
+ Unlike other CPLD fields, gain control doesn't use a register and instead
+ commands are directly sent over SPI. The format of these 24-bit commands is
+ as follows.
+ 23:22 Table select (1 = RX, 2 = TX)
+ 21:16 Gain index
+ 15:14 Reserved
+ 13 Write enable for DSA1
+ 12:7 Reserved
+ 6 Write enable for DSA2
+ 5:0 Reserved
+ The CPLD replies with the current attenuation settings as follows, but we
+ ignore this reply in our code.
+ 23:13 Reserved
+ 12:7 Current attenuator setting for DSA1
+ 6 Reserved
+ 5:0 Current attenuator setting for DSA2
+ */
+ //! Gain loader constants
+ constexpr size_t GAIN_CTRL_TABLE_FIELD = 22;
+ constexpr size_t GAIN_CTRL_INDEX_FIELD = 16;
+ constexpr size_t GAIN_CTRL_DSA1_FIELD = 13;
+ constexpr size_t GAIN_CTRL_DSA2_FIELD = 6;
+
+ constexpr uint32_t GAIN_CTRL_TABLE_RX = 1;
+ constexpr uint32_t GAIN_CTRL_TABLE_TX = 2;
+ constexpr uint32_t GAIN_CTRL_DSA1_WRITE_ENABLE = 1;
+ constexpr uint32_t GAIN_CTRL_DSA2_WRITE_ENABLE = 1;
+
+ /*
+ Similar to gain control, LO control doesn't use a register and instead
+ commands are directly sent over SPI. The format of these 24-bit commands is
+ as follows:
+ 23:22 Table select (Always 3 = LO)
+ 21:16 Attenuator setting
+ 15:14 Reserved
+ 13 Write enable for RX LO DSA
+ 12:7 Reserved
+ 6 Write enable for TX LO DSA
+ 5:0 Reserved
+ The CPLD replies with the current attenuator settings as follows, but we
+ ignore this reply in our code.
+ 23:13 Reserved
+ 12:7 Current attenuator setting for RX LO DSA
+ 6 Reserved
+ 5:0 Current attenuator setting for TX LO DSA
+ */
+ //! LO Gain loader constants
+ constexpr size_t LO_GAIN_CTRL_TABLE_FIELD = 22;
+ constexpr size_t LO_GAIN_CTRL_INDEX_FIELD = 16;
+ constexpr size_t LO_GAIN_CTRL_RX_LO_FIELD = 13;
+ constexpr size_t LO_GAIN_CTRL_TX_LO_FIELD = 6;
+
+ constexpr uint32_t LO_GAIN_CTRL_TABLE_LO = 3;
+ constexpr uint32_t LO_GAIN_CTRL_RX_LO_WRITE_ENABLE = 1;
+ constexpr uint32_t LO_GAIN_CTRL_RX_LO_WRITE_DISABLE = 0;
+ constexpr uint32_t LO_GAIN_CTRL_TX_LO_WRITE_ENABLE = 1;
+ constexpr uint32_t LO_GAIN_CTRL_TX_LO_WRITE_DISABLE = 0;
+}
+
+rhodium_cpld_ctrl::rhodium_cpld_ctrl(
+ write_spi_t write_fn,
+ read_spi_t read_fn
+)
+{
+ _write_reg_fn = [write_fn](const uint8_t addr, const uint32_t data){
+ UHD_LOG_TRACE("RH_CPLD",
+ str(boost::format("Writing to CPLD: 0x%02X -> 0x%04X")
+ % uint32_t(addr) % data));
+ const uint32_t spi_transaction = 0
+ | ((addr & 0x7F) << 17)
+ | data
+ ;
+ UHD_LOG_TRACE("RH_CPLD",
+ str(boost::format("SPI Transaction: 0x%04X")
+ % spi_transaction));
+ write_fn(spi_transaction);
+ };
+ _write_raw_fn = [write_fn](const uint32_t data) {
+ UHD_LOG_TRACE("RH_CPLD",
+ str(boost::format("Writing to CPLD: 0x%06X")
+ % data));
+ UHD_LOG_TRACE("RH_CPLD",
+ str(boost::format("SPI Transaction: 0x%06X")
+ % data));
+ write_fn(data);
+ };
+ _read_reg_fn = [read_fn](const uint8_t addr){
+ UHD_LOG_TRACE("RH_CPLD",
+ str(boost::format("Reading from CPLD address 0x%02X")
+ % uint32_t(addr)));
+ const uint32_t spi_transaction = (1<<16)
+ | ((addr & 0x7F) << 17)
+ ;
+ UHD_LOG_TRACE("RH_CPLD",
+ str(boost::format("SPI Transaction: 0x%04X")
+ % spi_transaction));
+ return read_fn(spi_transaction);
+ };
+
+ reset();
+ _loopback_test();
+}
+
+/******************************************************************************
+ * API
+ *****************************************************************************/
+void rhodium_cpld_ctrl::reset()
+{
+ std::lock_guard<std::mutex> l(_set_mutex);
+ UHD_LOG_TRACE("RH_CPLD", "Resetting CPLD to default state");
+ // Set local register cache to default values and commit
+ _regs = rhodium_cpld_regs_t();
+ _gain_queue.clear();
+ commit(true);
+}
+
+uint16_t rhodium_cpld_ctrl::get_reg(const uint8_t addr)
+{
+ std::lock_guard<std::mutex> l(_set_mutex);
+ return _read_reg_fn(addr);
+}
+
+void rhodium_cpld_ctrl::set_scratch(const uint16_t val)
+{
+ std::lock_guard<std::mutex> l(_set_mutex);
+ _regs.scratch = val;
+ commit();
+}
+
+uint16_t rhodium_cpld_ctrl::get_scratch()
+{
+ return get_reg(CPLD_REGS_SCRATCH);
+}
+
+void rhodium_cpld_ctrl::set_tx_switches(
+ const tx_sw2_t tx_sw2,
+ const tx_sw3_sw4_t tx_sw3_sw4,
+ const tx_sw5_t tx_sw5,
+ const tx_hb_lb_sel_t tx_hb_lb_sel,
+ const bool defer_commit
+) {
+ UHD_LOG_TRACE("RH_CPLD",
+ "Configuring TX band selection switches."\
+ " sw2=" << tx_sw2 <<
+ " sw3_sw4=" << tx_sw3_sw4 <<
+ " sw5=" << tx_sw5 <<
+ " hb_lb_sel=" << tx_hb_lb_sel
+ );
+
+ std::lock_guard<std::mutex> l(_set_mutex);
+
+ _regs.tx_sw2 = rhodium_cpld_regs_t::tx_sw2_t(tx_sw2);
+ _regs.tx_sw3_sw4 = rhodium_cpld_regs_t::tx_sw3_sw4_t(tx_sw3_sw4);
+ _regs.tx_sw5 = rhodium_cpld_regs_t::tx_sw5_t(tx_sw5);
+ _regs.tx_hb_lb_sel = rhodium_cpld_regs_t::tx_hb_lb_sel_t(tx_hb_lb_sel);
+
+ if (not defer_commit) {
+ commit();
+ }
+}
+
+void rhodium_cpld_ctrl::set_rx_switches(
+ const rx_sw2_sw7_t rx_sw2_sw7,
+ const rx_sw3_t rx_sw3,
+ const rx_sw4_sw5_t rx_sw4_sw5,
+ const rx_sw6_t rx_sw6,
+ const rx_hb_lb_sel_t rx_hb_lb_sel,
+ const bool defer_commit
+) {
+ UHD_LOG_TRACE("RH_CPLD",
+ "Configuring RX band selection switches."\
+ " sw2_sw7=" << rx_sw2_sw7 <<
+ " sw3=" << rx_sw3 <<
+ " sw4_sw5=" << rx_sw4_sw5 <<
+ " sw6=" << rx_sw6 <<
+ " hb_lb_sel=" << rx_hb_lb_sel
+ );
+
+ std::lock_guard<std::mutex> l(_set_mutex);
+
+ _regs.rx_sw2_sw7 = rhodium_cpld_regs_t::rx_sw2_sw7_t(rx_sw2_sw7);
+ _regs.rx_sw3 = rhodium_cpld_regs_t::rx_sw3_t(rx_sw3);
+ _regs.rx_sw4_sw5 = rhodium_cpld_regs_t::rx_sw4_sw5_t(rx_sw4_sw5);
+ _regs.rx_sw6 = rhodium_cpld_regs_t::rx_sw6_t(rx_sw6);
+ _regs.rx_hb_lb_sel = rhodium_cpld_regs_t::rx_hb_lb_sel_t(rx_hb_lb_sel);
+
+ if (not defer_commit) {
+ commit();
+ }
+}
+
+void rhodium_cpld_ctrl::set_rx_input_switches(
+ const rx_sw1_t rx_sw1,
+ const cal_iso_sw_t cal_iso_sw,
+ const bool defer_commit
+) {
+ UHD_LOG_TRACE("RH_CPLD",
+ "Configuring RX input selection switches."\
+ " sw1=" << rx_sw1 <<
+ " cal_iso=" << cal_iso_sw
+ );
+
+ std::lock_guard<std::mutex> l(_set_mutex);
+
+ _regs.rx_sw1 = rhodium_cpld_regs_t::rx_sw1_t(rx_sw1);
+ _regs.cal_iso_sw = rhodium_cpld_regs_t::cal_iso_sw_t(cal_iso_sw);
+
+ if (not defer_commit) {
+ commit();
+ }
+}
+
+void rhodium_cpld_ctrl::set_tx_output_switches(
+ const tx_sw1_t tx_sw1,
+ const bool defer_commit
+) {
+ UHD_LOG_TRACE("RH_CPLD",
+ "Configuring TX output selection switches."\
+ " sw1=" << tx_sw1
+ );
+
+ std::lock_guard<std::mutex> l(_set_mutex);
+
+ _regs.tx_sw1 = rhodium_cpld_regs_t::tx_sw1_t(tx_sw1);
+
+ if (not defer_commit) {
+ commit();
+ }
+}
+
+void rhodium_cpld_ctrl::set_rx_lo_source(
+ const rx_lo_input_sel_t rx_lo_input_sel,
+ const bool defer_commit
+) {
+ UHD_LOG_TRACE("RH_CPLD", "Setting RX LO source to " << rx_lo_input_sel);
+ std::lock_guard<std::mutex> l(_set_mutex);
+
+ _regs.rx_lo_input_sel = rhodium_cpld_regs_t::rx_lo_input_sel_t(rx_lo_input_sel);
+
+ if (not defer_commit) {
+ commit();
+ }
+}
+
+void rhodium_cpld_ctrl::set_tx_lo_source(
+ const tx_lo_input_sel_t tx_lo_input_sel,
+ const bool defer_commit
+) {
+ UHD_LOG_TRACE("RH_CPLD", "Setting TX LO source to " << tx_lo_input_sel);
+ std::lock_guard<std::mutex> l(_set_mutex);
+
+ _regs.tx_lo_input_sel = rhodium_cpld_regs_t::tx_lo_input_sel_t(tx_lo_input_sel);
+
+ if (not defer_commit) {
+ commit();
+ }
+}
+
+void rhodium_cpld_ctrl::set_rx_lo_path(
+ const double freq,
+ const bool defer_commit
+) {
+ UHD_LOG_TRACE("RH_CPLD", "Configuring RX LO filter and settings. freq=" << freq);
+ std::lock_guard<std::mutex> l(_set_mutex);
+
+ auto freq_compare = fp_compare_epsilon<double>(freq, RHODIUM_FREQ_COMPARE_EPSILON);
+
+ if (freq_compare < RX_DEMOD_ADJ_1500OHM_THRESHOLD) {
+ _regs.rx_demod_adj = rhodium_cpld_regs_t::rx_demod_adj_t::RX_DEMOD_ADJ_RES_1500_OHM;
+ } else if (freq_compare < RX_DEMOD_ADJ_200OHM_THRESHOLD) {
+ _regs.rx_demod_adj = rhodium_cpld_regs_t::rx_demod_adj_t::RX_DEMOD_ADJ_RES_200_OHM;
+ } else {
+ _regs.rx_demod_adj = rhodium_cpld_regs_t::rx_demod_adj_t::RX_DEMOD_ADJ_RES_OPEN;
+ }
+
+ if (freq_compare < RHODIUM_LO_0_9_GHZ_LPF_THRESHOLD_FREQ) {
+ _regs.rx_lo_filter_sel = rhodium_cpld_regs_t::rx_lo_filter_sel_t::RX_LO_FILTER_SEL_0_9GHZ_LPF;
+ } else if (freq_compare < RHODIUM_LO_2_25_GHZ_LPF_THRESHOLD_FREQ) {
+ _regs.rx_lo_filter_sel = rhodium_cpld_regs_t::rx_lo_filter_sel_t::RX_LO_FILTER_SEL_2_25GHZ_LPF;
+ } else {
+ _regs.rx_lo_filter_sel = rhodium_cpld_regs_t::rx_lo_filter_sel_t::RX_LO_FILTER_SEL_5_85GHZ_LPF;
+ }
+
+ if (not defer_commit) {
+ commit();
+ }
+}
+
+void rhodium_cpld_ctrl::set_tx_lo_path(
+ const double freq,
+ const bool defer_commit
+) {
+ UHD_LOG_TRACE("RH_CPLD", "Configuring TX LO filter and settings. freq=" << freq);
+ std::lock_guard<std::mutex> l(_set_mutex);
+
+ auto freq_compare = fp_compare_epsilon<double>(freq, RHODIUM_FREQ_COMPARE_EPSILON);
+
+ if (freq_compare < RHODIUM_LO_0_9_GHZ_LPF_THRESHOLD_FREQ) {
+ _regs.tx_lo_filter_sel = rhodium_cpld_regs_t::tx_lo_filter_sel_t::TX_LO_FILTER_SEL_0_9GHZ_LPF;
+ } else if (freq_compare < RHODIUM_LO_2_25_GHZ_LPF_THRESHOLD_FREQ) {
+ _regs.tx_lo_filter_sel = rhodium_cpld_regs_t::tx_lo_filter_sel_t::TX_LO_FILTER_SEL_2_25GHZ_LPF;
+ } else {
+ _regs.tx_lo_filter_sel = rhodium_cpld_regs_t::tx_lo_filter_sel_t::TX_LO_FILTER_SEL_5_85GHZ_LPF;
+ }
+
+ if (not defer_commit) {
+ commit();
+ }
+}
+
+void rhodium_cpld_ctrl::set_gain_index(
+ const uint32_t index,
+ const gain_band_t band,
+ const uhd::direction_t dir,
+ const bool defer_commit
+) {
+ UHD_ASSERT_THROW(index <= MAX_GAIN_INDEX);
+ UHD_ASSERT_THROW(dir == RX_DIRECTION or dir == TX_DIRECTION);
+
+ if (band == HIGH)
+ {
+ if (dir == RX_DIRECTION)
+ {
+ _regs.rx_gain_tbl_sel = rhodium_cpld_regs_t::RX_GAIN_TBL_SEL_HIGHBAND;
+ }
+ else {
+ _regs.tx_gain_tbl_sel = rhodium_cpld_regs_t::TX_GAIN_TBL_SEL_HIGHBAND;
+ }
+ } else {
+ if (dir == RX_DIRECTION)
+ {
+ _regs.rx_gain_tbl_sel = rhodium_cpld_regs_t::RX_GAIN_TBL_SEL_LOWBAND;
+ }
+ else {
+ _regs.tx_gain_tbl_sel = rhodium_cpld_regs_t::TX_GAIN_TBL_SEL_LOWBAND;
+ }
+ }
+
+ const uint8_t table_id = (dir == RX_DIRECTION) ?
+ GAIN_CTRL_TABLE_RX :
+ GAIN_CTRL_TABLE_TX;
+
+ const uint32_t cmd =
+ (table_id << GAIN_CTRL_TABLE_FIELD) |
+ (index << GAIN_CTRL_INDEX_FIELD) |
+ (GAIN_CTRL_DSA1_WRITE_ENABLE << GAIN_CTRL_DSA1_FIELD) |
+ (GAIN_CTRL_DSA2_WRITE_ENABLE << GAIN_CTRL_DSA2_FIELD);
+
+ std::lock_guard<std::mutex> l(_set_mutex);
+ _gain_queue.emplace_back(cmd);
+
+ if (not defer_commit) {
+ commit();
+ }
+}
+
+void rhodium_cpld_ctrl::set_lo_gain(
+ const uint32_t index,
+ const uhd::direction_t dir,
+ const bool defer_commit
+) {
+ UHD_ASSERT_THROW(index <= MAX_LO_GAIN_INDEX);
+
+ // The DSA has 0-30 dB of attenuation in 1 dB steps
+ // This index directly controls the attenuation value of the LO DSA,
+ // so reverse the gain value to write the value
+ const uint32_t attenuation = MAX_LO_GAIN_INDEX - index;
+ const uint8_t set_rx = (dir == RX_DIRECTION or dir == DX_DIRECTION) ?
+ LO_GAIN_CTRL_RX_LO_WRITE_ENABLE :
+ LO_GAIN_CTRL_RX_LO_WRITE_DISABLE;
+ const uint8_t set_tx = (dir == TX_DIRECTION or dir == DX_DIRECTION) ?
+ LO_GAIN_CTRL_TX_LO_WRITE_ENABLE :
+ LO_GAIN_CTRL_TX_LO_WRITE_DISABLE;
+
+ const uint32_t cmd =
+ (LO_GAIN_CTRL_TABLE_LO << LO_GAIN_CTRL_TABLE_FIELD) |
+ (attenuation << LO_GAIN_CTRL_INDEX_FIELD) |
+ (set_rx << LO_GAIN_CTRL_RX_LO_FIELD) |
+ (set_tx << LO_GAIN_CTRL_TX_LO_FIELD);
+
+ std::lock_guard<std::mutex> l(_set_mutex);
+ _gain_queue.emplace_back(cmd);
+
+ if (not defer_commit) {
+ commit();
+ }
+}
+
+
+/******************************************************************************
+ * Private methods
+ *****************************************************************************/
+void rhodium_cpld_ctrl::_loopback_test()
+{
+ UHD_LOG_TRACE("RH_CPLD", "Performing CPLD scratch loopback test...");
+ using namespace std::chrono;
+ const uint16_t random_number = // Derived from current time
+ uint16_t(system_clock::to_time_t(system_clock::now()) & 0xFFFF);
+ set_scratch(random_number);
+ const uint16_t actual = get_scratch();
+ if (actual != random_number) {
+ UHD_LOGGER_ERROR("RH_CPLD")
+ << "CPLD scratch loopback failed! "
+ << boost::format("Expected: 0x%04X Got: 0x%04X")
+ % random_number % actual
+ ;
+ throw uhd::runtime_error("CPLD scratch loopback failed!");
+ }
+ UHD_LOG_TRACE("RH_CPLD", "CPLD scratch loopback test passed!");
+}
+
+void rhodium_cpld_ctrl::commit(const bool save_all)
+{
+ UHD_LOGGER_TRACE("RH_CPLD")
+ << "Storing register cache "
+ << (save_all ? "completely" : "selectively")
+ << " to CPLD via SPI..."
+ ;
+ auto changed_addrs = save_all ?
+ _regs.get_all_addrs() :
+ _regs.get_changed_addrs<size_t>();
+ for (const auto addr: changed_addrs) {
+ _write_reg_fn(addr, _regs.get_reg(addr));
+ }
+ _regs.save_state();
+ UHD_LOG_TRACE("RH_CPLD",
+ "Storing cache complete: " \
+ "Updated " << changed_addrs.size() << " registers.")
+ ;
+
+ UHD_LOGGER_TRACE("RH_CPLD")
+ << "Writing queued gain commands "
+ << "to CPLD via SPI..."
+ ;
+ for (const auto cmd : _gain_queue) {
+ _write_raw_fn(cmd);
+ }
+ UHD_LOG_TRACE("RH_CPLD",
+ "Writing queued gain commands complete: " \
+ "Wrote " << _gain_queue.size() << " commands.")
+ ;
+ _gain_queue.clear();
+}
+
diff --git a/host/lib/usrp/dboard/rhodium/rhodium_cpld_ctrl.hpp b/host/lib/usrp/dboard/rhodium/rhodium_cpld_ctrl.hpp
new file mode 100644
index 000000000..a11f29d7d
--- /dev/null
+++ b/host/lib/usrp/dboard/rhodium/rhodium_cpld_ctrl.hpp
@@ -0,0 +1,344 @@
+//
+// Copyright 2018 Ettus Research, a National Instruments Company
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+#ifndef INCLUDED_LIBUHD_RHODIUM_CPLD_CTRL_HPP
+#define INCLUDED_LIBUHD_RHODIUM_CPLD_CTRL_HPP
+
+#include "adf4351_regs.hpp"
+#include "rhodium_cpld_regs.hpp"
+#include <uhd/types/serial.hpp>
+#include <uhd/types/direction.hpp>
+#include <mutex>
+#include <memory>
+
+//! Controls the CPLD on a Rhodium daughterboard
+//
+// Setters are thread-safe through lock guards. This lets a CPLD control object
+// be shared by multiple owners.
+class rhodium_cpld_ctrl
+{
+public:
+ /**************************************************************************
+ * Types
+ *************************************************************************/
+ using sptr = std::shared_ptr<rhodium_cpld_ctrl>;
+ //! SPI write function: Can take a SPI transaction and clock it out
+ using write_spi_t = std::function<void(uint32_t)>;
+ //! SPI read function: Return SPI
+ using read_spi_t = std::function<uint32_t(uint32_t)>;
+
+ enum gain_band_t {
+ LOW,
+ HIGH,
+ };
+
+ enum tx_sw1_t {
+ TX_SW1_TOLOWBAND = 0,
+ TX_SW1_TOSWITCH2 = 1,
+ TX_SW1_TOCALLOOPBACK = 2,
+ TX_SW1_ISOLATION = 3
+ };
+
+ enum tx_sw2_t {
+ TX_SW2_FROMSWITCH3 = 0,
+ TX_SW2_FROMTXFILTERLP6000MHZ = 1,
+ TX_SW2_FROMTXFILTERLP4100MHZ = 2,
+ TX_SW2_FROMTXFILTERLP3000MHZ = 3
+ };
+
+ enum tx_sw3_sw4_t {
+ TX_SW3_SW4_FROMTXFILTERLP1000MHZ = 1,
+ TX_SW3_SW4_FROMTXFILTERLP0650MHZ = 2,
+ TX_SW3_SW4_FROMTXFILTERLP1900MHZ = 4,
+ TX_SW3_SW4_FROMTXFILTERLP1350MHZ = 8
+ };
+
+ enum tx_sw5_t {
+ TX_SW5_TOTXFILTERLP3000MHZ = 0,
+ TX_SW5_TOTXFILTERLP4100MHZ = 1,
+ TX_SW5_TOTXFILTERLP6000MHZ = 2,
+ TX_SW5_TOSWITCH4 = 3
+ };
+
+ enum rx_sw1_t {
+ RX_SW1_FROMCALLOOPBACK = 0,
+ RX_SW1_FROMRX2INPUT = 1,
+ RX_SW1_ISOLATION = 2,
+ RX_SW1_FROMTXRXINPUT = 3
+ };
+
+ enum rx_sw2_sw7_t {
+ RX_SW2_SW7_LOWBANDFILTERBANK = 0,
+ RX_SW2_SW7_HIGHBANDFILTERBANK = 1
+ };
+
+ enum rx_sw3_t {
+ RX_SW3_TOSWITCH4 = 0,
+ RX_SW3_TOFILTER4500X6000MHZ = 1,
+ RX_SW3_TOFILTER3000X4500MHZ = 2,
+ RX_SW3_TOFILTER2050X3000MHZ = 3
+ };
+
+ enum rx_sw4_sw5_t {
+ RX_SW4_SW5_FILTER0760X1100MHZ = 1,
+ RX_SW4_SW5_FILTER0450X0760MHZ = 2,
+ RX_SW4_SW5_FILTER1410X2050MHZ = 4,
+ RX_SW4_SW5_FILTER1100X1410MHZ = 8
+ };
+
+ enum rx_sw6_t {
+ RX_SW6_FROMFILTER2050X3000MHZ = 0,
+ RX_SW6_FROMFILTER3000X4500MHZ = 1,
+ RX_SW6_FROMFILTER4500X6000MHZ = 2,
+ RX_SW6_FROMSWITCH5 = 3,
+ };
+
+ enum cal_iso_sw_t {
+ CAL_ISO_ISOLATION = 0,
+ CAL_ISO_CALLOOPBACK = 1
+ };
+
+ enum tx_hb_lb_sel_t {
+ TX_HB_LB_SEL_LOWBAND = 0,
+ TX_HB_LB_SEL_HIGHBAND = 1
+ };
+
+ enum tx_lo_input_sel_t {
+ TX_LO_INPUT_SEL_INTERNAL = 0,
+ TX_LO_INPUT_SEL_EXTERNAL = 1
+ };
+
+ enum rx_hb_lb_sel_t {
+ RX_HB_LB_SEL_LOWBAND = 0,
+ RX_HB_LB_SEL_HIGHBAND = 1
+ };
+
+ enum rx_lo_input_sel_t {
+ RX_LO_INPUT_SEL_INTERNAL = 1,
+ RX_LO_INPUT_SEL_EXTERNAL = 0
+ };
+
+ enum rx_demod_adj {
+ RX_DEMOD_OPEN = 0,
+ RX_DEMOD_200OHM = 1,
+ RX_DEMOD_1500OHM = 2
+ };
+
+ enum tx_lo_filter_sel_t {
+ TX_LO_FILTER_SEL_0_9GHZ_LPF = 0,
+ TX_LO_FILTER_SEL_5_85GHZ_LPF = 1,
+ TX_LO_FILTER_SEL_2_25GHZ_LPF = 2,
+ TX_LO_FILTER_SEL_ISOLATION = 3
+ };
+
+ enum rx_lo_filter_sel_t {
+ RX_LO_FILTER_SEL_0_9GHZ_LPF = 0,
+ RX_LO_FILTER_SEL_5_85GHZ_LPF = 1,
+ RX_LO_FILTER_SEL_2_25GHZ_LPF = 2,
+ RX_LO_FILTER_SEL_ISOLATION = 3
+ };
+
+ /*! Constructor.
+ *
+ * \param write_spi_fn SPI write function
+ * \param read_spi_fn SPI read function
+ */
+ rhodium_cpld_ctrl(
+ write_spi_t write_spi_fn,
+ read_spi_t read_spi_fn
+ );
+
+ /**************************************************************************
+ * API
+ *************************************************************************/
+ //! Reset all registers to their default state
+ void reset();
+
+ //! Return the current value of register at \p addr.
+ //
+ // Note: This will initiate a SPI transaction, it doesn't read from the
+ // internal register cache. However, it won't actually update the register
+ // cache.
+ uint16_t get_reg(const uint8_t addr);
+
+ //! Set the value of the scratch register (has no effect on chip functions)
+ void set_scratch(const uint16_t val);
+
+ //! Get the value of the scratch reg.
+ //
+ // This should be zero unless set_scratch() was called beforehand (note
+ // that _loopback_test() will also call set_scratch()). If set_scratch()
+ // was previously called, this should return the previously written value.
+ //
+ // Note: This will call get_reg(), and not simply return the value of the
+ // internal cache.
+ uint16_t get_scratch();
+
+ /*! Frequency-related settings, transmit side
+ *
+ * \param tx_sw2 Filter bank switch 2
+ * \param tx_sw3_sw4 Filter bank switch 3 and 4
+ * \param tx_sw5 Filter bank switch 5
+ * \param tx_hb_lb_sel Power on the highband or lowband amplifier
+ * \param tx_lo_filter_sel Select LPF filter for LO
+ */
+ void set_tx_switches(
+ const tx_sw2_t tx_sw2,
+ const tx_sw3_sw4_t tx_sw3_sw4,
+ const tx_sw5_t tx_sw5,
+ const tx_hb_lb_sel_t tx_hb_lb_sel,
+ const bool defer_commit = false
+ );
+
+ /*! Frequency-related settings, receive side
+ *
+ * \param rx_sw2_sw7 Filter bank switch 2 and 7
+ * \param rx_sw3 Filter bank switch 3
+ * \param rx_sw4_sw5 Filter bank switch 4 and 5
+ * \param rx_sw6 Filter bank switch 6
+ * \param rx_hb_lb_sel Power on the highband or lowband amplifier
+ * \param rx_lo_filter_sel Select LPF filter for LO
+ */
+ void set_rx_switches(
+ const rx_sw2_sw7_t rx_sw2_sw7,
+ const rx_sw3_t rx_sw3,
+ const rx_sw4_sw5_t rx_sw4_sw5,
+ const rx_sw6_t rx_sw6,
+ const rx_hb_lb_sel_t rx_hb_lb_sel,
+ const bool defer_commit = false
+ );
+
+ /*! Input switches for RX side
+ *
+ * Note: These are not frequency dependent.
+ *
+ * \param rx_sw1 Input selection of RX path
+ * \param cal_iso_sw Terminates the calibration loopback path
+ */
+ void set_rx_input_switches(
+ const rx_sw1_t rx_sw1,
+ const cal_iso_sw_t cal_iso_sw,
+ const bool defer_commit = false
+ );
+
+ /*! Output switches for TX side
+ *
+ * Note: These are not frequency dependent.
+ *
+ * \param tx_sw1 Output selection of TX path
+ */
+ void set_tx_output_switches(
+ const tx_sw1_t tx_sw1,
+ const bool defer_commit = false
+ );
+
+ /*! Input switch for RX LO
+ *
+ * \param rx_lo_input_sel Selects RX LO source
+ */
+ void set_rx_lo_source(
+ const rx_lo_input_sel_t rx_lo_input_sel,
+ const bool defer_commit = false
+ );
+
+ /*! Input switch for TX LO
+ *
+ * \param tx_lo_input_sel Selects TX LO source
+ */
+ void set_tx_lo_source(
+ const tx_lo_input_sel_t tx_lo_input_sel,
+ const bool defer_commit = false
+ );
+
+ /*! Configure RX LO filter, synth, and mixer settings
+ *
+ * \param freq RX LO Frequency
+ */
+ void set_rx_lo_path(
+ const double freq,
+ const bool defer_commit = false
+ );
+
+ /*! Configure TX LO filter, synth, and mixer settings
+ *
+ * \param freq TX LO Frequency
+ */
+ void set_tx_lo_path(
+ const double freq,
+ const bool defer_commit = false
+ );
+
+
+ /*! Gain index setting for the RF frontend
+ *
+ * Sets the gain index to one of the predefined values that have been
+ * loaded into the CPLD by gain table loader in MPM.
+ *
+ * \param index Index of the gain table entry to apply (0-60)
+ * \param band Selects which table to use (lowband or highband)
+ * \param dir Selects which RF frontend to apply to (RX or TX)
+ */
+ void set_gain_index(
+ const uint32_t index,
+ const gain_band_t band,
+ const uhd::direction_t dir,
+ const bool defer_commit = false
+ );
+
+ /*! Gain setting for LO1
+ *
+ * Sets the attenuation of the RX LO1 DSA or TX LO1 DSA.
+ *
+ * Note: This function uses gain as a parameter, although it is
+ * setting an attenuation.
+ *
+ * \param index Gain value to apply (0-30)
+ * \param dir Selects which LO to apply to (RX, TX, or DX)
+ */
+ void set_lo_gain(
+ const uint32_t index,
+ const uhd::direction_t dir,
+ const bool defer_commit = false
+ );
+
+private:
+ //! Write function: Take address / data pair, craft SPI transaction
+ using write_reg_fn_t = std::function<void(uint32_t, uint32_t)>;
+ //! Write function: Send bits directly to CPLD
+ using write_raw_fn_t = std::function<void(uint32_t)>;
+ //! Read function: Return value given address
+ using read_reg_fn_t = std::function<uint32_t(uint32_t)>;
+
+ //! Dump the state of the registers into the CPLD
+ //
+ // \param save_all If true, save all registers. If false, only change those
+ // that changes recently.
+ void commit(const bool save_all = false);
+
+ //! Writes to the scratch reg and reads again. Throws on failure.
+ //
+ // Note: This is not thread-safe. Accesses to the scratch reg are not
+ // atomic. Only call this from a thread-safe environment, please.
+ void _loopback_test();
+
+ //! Write function for regs pokes
+ write_reg_fn_t _write_reg_fn;
+ //! Read function for regs peeks
+ read_reg_fn_t _read_reg_fn;
+ //! Write function for raw poke command
+ write_raw_fn_t _write_raw_fn;
+
+ //! Current state of the CPLD registers (for write operations only)
+ rhodium_cpld_regs_t _regs;
+
+ //! Queue of gain commands to be written at next commit
+ std::vector<uint32_t> _gain_queue;
+
+ //! Lock access to setters
+ std::mutex _set_mutex;
+};
+
+#endif /* INCLUDED_LIBUHD_RHODIUM_CPLD_CTRL_HPP */
diff --git a/host/lib/usrp/dboard/rhodium/rhodium_radio_ctrl_cpld.cpp b/host/lib/usrp/dboard/rhodium/rhodium_radio_ctrl_cpld.cpp
new file mode 100644
index 000000000..68efe0f83
--- /dev/null
+++ b/host/lib/usrp/dboard/rhodium/rhodium_radio_ctrl_cpld.cpp
@@ -0,0 +1,374 @@
+//
+// Copyright 2018 Ettus Research, a National Instruments Company
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+#include "rhodium_radio_ctrl_impl.hpp"
+#include "rhodium_cpld_ctrl.hpp"
+#include "rhodium_constants.hpp"
+#include <uhd/utils/log.hpp>
+
+using namespace uhd;
+using namespace uhd::usrp;
+using namespace uhd::rfnoc;
+
+namespace {
+ const char* rx_band_to_log(rhodium_radio_ctrl_impl::rx_band rx_band)
+ {
+ switch (rx_band)
+ {
+ case rhodium_radio_ctrl_impl::rx_band::RX_BAND_0:
+ return "0";
+ case rhodium_radio_ctrl_impl::rx_band::RX_BAND_1:
+ return "1";
+ case rhodium_radio_ctrl_impl::rx_band::RX_BAND_2:
+ return "2";
+ case rhodium_radio_ctrl_impl::rx_band::RX_BAND_3:
+ return "3";
+ case rhodium_radio_ctrl_impl::rx_band::RX_BAND_4:
+ return "4";
+ case rhodium_radio_ctrl_impl::rx_band::RX_BAND_5:
+ return "5";
+ case rhodium_radio_ctrl_impl::rx_band::RX_BAND_6:
+ return "6";
+ case rhodium_radio_ctrl_impl::rx_band::RX_BAND_7:
+ return "7";
+ case rhodium_radio_ctrl_impl::rx_band::RX_BAND_INVALID:
+ return "INVALID";
+ default:
+ UHD_THROW_INVALID_CODE_PATH();
+ }
+ }
+
+ const char* tx_band_to_log(rhodium_radio_ctrl_impl::tx_band tx_band)
+ {
+ switch (tx_band)
+ {
+ case rhodium_radio_ctrl_impl::tx_band::TX_BAND_0:
+ return "0";
+ case rhodium_radio_ctrl_impl::tx_band::TX_BAND_1:
+ return "1";
+ case rhodium_radio_ctrl_impl::tx_band::TX_BAND_2:
+ return "2";
+ case rhodium_radio_ctrl_impl::tx_band::TX_BAND_3:
+ return "3";
+ case rhodium_radio_ctrl_impl::tx_band::TX_BAND_4:
+ return "4";
+ case rhodium_radio_ctrl_impl::tx_band::TX_BAND_5:
+ return "5";
+ case rhodium_radio_ctrl_impl::tx_band::TX_BAND_6:
+ return "6";
+ case rhodium_radio_ctrl_impl::tx_band::TX_BAND_7:
+ return "7";
+ case rhodium_radio_ctrl_impl::tx_band::TX_BAND_INVALID:
+ return "INVALID";
+ default:
+ UHD_THROW_INVALID_CODE_PATH();
+ }
+ }
+}
+
+void rhodium_radio_ctrl_impl::_update_rx_freq_switches(
+ const double freq
+) {
+ UHD_LOG_TRACE(unique_id(),
+ "Update all RX freq related switches. f=" << freq << " Hz, "
+ );
+ const auto band = _map_freq_to_rx_band(freq);
+ UHD_LOG_TRACE(unique_id(),
+ "Selected band " << rx_band_to_log(band));
+
+ // select values for lowband/highband switches
+ const bool is_lowband = (band == rx_band::RX_BAND_0);
+ auto rx_sw2_sw7 = is_lowband ?
+ rhodium_cpld_ctrl::RX_SW2_SW7_LOWBANDFILTERBANK :
+ rhodium_cpld_ctrl::RX_SW2_SW7_HIGHBANDFILTERBANK;
+ auto rx_hb_lb_sel = is_lowband ?
+ rhodium_cpld_ctrl::RX_HB_LB_SEL_LOWBAND :
+ rhodium_cpld_ctrl::RX_HB_LB_SEL_HIGHBAND;
+
+ // select values for filter bank switches
+ rhodium_cpld_ctrl::rx_sw3_t rx_sw3;
+ rhodium_cpld_ctrl::rx_sw4_sw5_t rx_sw4_sw5;
+ rhodium_cpld_ctrl::rx_sw6_t rx_sw6;
+ switch (band)
+ {
+ case rx_band::RX_BAND_0:
+ // Low band doesn't use the filter banks, use configuration for band 1
+ case rx_band::RX_BAND_1:
+ rx_sw3 = rhodium_cpld_ctrl::RX_SW3_TOSWITCH4;
+ rx_sw4_sw5 = rhodium_cpld_ctrl::RX_SW4_SW5_FILTER0450X0760MHZ;
+ rx_sw6 = rhodium_cpld_ctrl::RX_SW6_FROMSWITCH5;
+ break;
+ case rx_band::RX_BAND_2:
+ rx_sw3 = rhodium_cpld_ctrl::RX_SW3_TOSWITCH4;
+ rx_sw4_sw5 = rhodium_cpld_ctrl::RX_SW4_SW5_FILTER0760X1100MHZ;
+ rx_sw6 = rhodium_cpld_ctrl::RX_SW6_FROMSWITCH5;
+ break;
+ case rx_band::RX_BAND_3:
+ rx_sw3 = rhodium_cpld_ctrl::RX_SW3_TOSWITCH4;
+ rx_sw4_sw5 = rhodium_cpld_ctrl::RX_SW4_SW5_FILTER1100X1410MHZ;
+ rx_sw6 = rhodium_cpld_ctrl::RX_SW6_FROMSWITCH5;
+ break;
+ case rx_band::RX_BAND_4:
+ rx_sw3 = rhodium_cpld_ctrl::RX_SW3_TOSWITCH4;
+ rx_sw4_sw5 = rhodium_cpld_ctrl::RX_SW4_SW5_FILTER1410X2050MHZ;
+ rx_sw6 = rhodium_cpld_ctrl::RX_SW6_FROMSWITCH5;
+ break;
+ case rx_band::RX_BAND_5:
+ rx_sw3 = rhodium_cpld_ctrl::RX_SW3_TOFILTER2050X3000MHZ;
+ rx_sw4_sw5 = rhodium_cpld_ctrl::RX_SW4_SW5_FILTER0450X0760MHZ;
+ rx_sw6 = rhodium_cpld_ctrl::RX_SW6_FROMFILTER2050X3000MHZ;
+ break;
+ case rx_band::RX_BAND_6:
+ rx_sw3 = rhodium_cpld_ctrl::RX_SW3_TOFILTER3000X4500MHZ;
+ rx_sw4_sw5 = rhodium_cpld_ctrl::RX_SW4_SW5_FILTER0450X0760MHZ;
+ rx_sw6 = rhodium_cpld_ctrl::RX_SW6_FROMFILTER3000X4500MHZ;
+ break;
+ case rx_band::RX_BAND_7:
+ rx_sw3 = rhodium_cpld_ctrl::RX_SW3_TOFILTER4500X6000MHZ;
+ rx_sw4_sw5 = rhodium_cpld_ctrl::RX_SW4_SW5_FILTER0450X0760MHZ;
+ rx_sw6 = rhodium_cpld_ctrl::RX_SW6_FROMFILTER4500X6000MHZ;
+ break;
+ case rx_band::RX_BAND_INVALID:
+ throw uhd::runtime_error(str(boost::format(
+ "Cannot map RX frequency to band: %f") % freq));
+ default:
+ UHD_THROW_INVALID_CODE_PATH();
+ }
+
+ // commit settings to cpld
+ _cpld->set_rx_switches(
+ rx_sw2_sw7,
+ rx_sw3,
+ rx_sw4_sw5,
+ rx_sw6,
+ rx_hb_lb_sel
+ );
+}
+
+void rhodium_radio_ctrl_impl::_update_tx_freq_switches(
+ const double freq
+){
+ UHD_LOG_TRACE(unique_id(),
+ "Update all TX freq related switches. f=" << freq << " Hz, "
+ );
+
+ const auto band = _map_freq_to_tx_band(freq);
+
+ UHD_LOG_TRACE(unique_id(),
+ "Selected band " << tx_band_to_log(band));
+
+ // select values for lowband/highband switches
+ const bool is_lowband = (band == tx_band::TX_BAND_0);
+ auto tx_hb_lb_sel = is_lowband ?
+ rhodium_cpld_ctrl::TX_HB_LB_SEL_LOWBAND :
+ rhodium_cpld_ctrl::TX_HB_LB_SEL_HIGHBAND;
+
+ // select values for filter bank switches
+ rhodium_cpld_ctrl::tx_sw2_t tx_sw2;
+ rhodium_cpld_ctrl::tx_sw3_sw4_t tx_sw3_sw4;
+ rhodium_cpld_ctrl::tx_sw5_t tx_sw5;
+ switch (band)
+ {
+ case tx_band::TX_BAND_0:
+ // Low band doesn't use the filter banks, use configuration for band 1
+ case tx_band::TX_BAND_1:
+ tx_sw2 = rhodium_cpld_ctrl::TX_SW2_FROMSWITCH3;
+ tx_sw3_sw4 = rhodium_cpld_ctrl::TX_SW3_SW4_FROMTXFILTERLP0650MHZ;
+ tx_sw5 = rhodium_cpld_ctrl::TX_SW5_TOSWITCH4;
+ break;
+ case tx_band::TX_BAND_2:
+ tx_sw2 = rhodium_cpld_ctrl::TX_SW2_FROMSWITCH3;
+ tx_sw3_sw4 = rhodium_cpld_ctrl::TX_SW3_SW4_FROMTXFILTERLP1000MHZ;
+ tx_sw5 = rhodium_cpld_ctrl::TX_SW5_TOSWITCH4;
+ break;
+ case tx_band::TX_BAND_3:
+ tx_sw2 = rhodium_cpld_ctrl::TX_SW2_FROMSWITCH3;
+ tx_sw3_sw4 = rhodium_cpld_ctrl::TX_SW3_SW4_FROMTXFILTERLP1350MHZ;
+ tx_sw5 = rhodium_cpld_ctrl::TX_SW5_TOSWITCH4;
+ break;
+ case tx_band::TX_BAND_4:
+ tx_sw2 = rhodium_cpld_ctrl::TX_SW2_FROMSWITCH3;
+ tx_sw3_sw4 = rhodium_cpld_ctrl::TX_SW3_SW4_FROMTXFILTERLP1900MHZ;
+ tx_sw5 = rhodium_cpld_ctrl::TX_SW5_TOSWITCH4;
+ break;
+ case tx_band::TX_BAND_5:
+ tx_sw2 = rhodium_cpld_ctrl::TX_SW2_FROMTXFILTERLP3000MHZ;
+ tx_sw3_sw4 = rhodium_cpld_ctrl::TX_SW3_SW4_FROMTXFILTERLP0650MHZ;
+ tx_sw5 = rhodium_cpld_ctrl::TX_SW5_TOTXFILTERLP3000MHZ;
+ break;
+ case tx_band::TX_BAND_6:
+ tx_sw2 = rhodium_cpld_ctrl::TX_SW2_FROMTXFILTERLP4100MHZ;
+ tx_sw3_sw4 = rhodium_cpld_ctrl::TX_SW3_SW4_FROMTXFILTERLP0650MHZ;
+ tx_sw5 = rhodium_cpld_ctrl::TX_SW5_TOTXFILTERLP4100MHZ;
+ break;
+ case tx_band::TX_BAND_7:
+ tx_sw2 = rhodium_cpld_ctrl::TX_SW2_FROMTXFILTERLP6000MHZ;
+ tx_sw3_sw4 = rhodium_cpld_ctrl::TX_SW3_SW4_FROMTXFILTERLP0650MHZ;
+ tx_sw5 = rhodium_cpld_ctrl::TX_SW5_TOTXFILTERLP6000MHZ;
+ break;
+ case tx_band::TX_BAND_INVALID:
+ throw uhd::runtime_error(str(boost::format(
+ "Cannot map TX frequency to band: %f") % freq));
+ default:
+ UHD_THROW_INVALID_CODE_PATH();
+ }
+
+ // commit settings to cpld
+ _cpld->set_tx_switches(
+ tx_sw2,
+ tx_sw3_sw4,
+ tx_sw5,
+ tx_hb_lb_sel
+ );
+
+ // If TX lowband/highband changes, SW10 needs to be updated
+ _update_tx_output_switches(get_tx_antenna(0));
+}
+
+void rhodium_radio_ctrl_impl::_update_rx_input_switches(
+ const std::string &input
+) {
+ UHD_LOG_TRACE(unique_id(),
+ "Update all RX input related switches. input=" << input
+ );
+ const sw10_t sw10 = (input == "TX/RX") ?
+ SW10_TORX :
+ SW10_ISOLATION;
+ const rhodium_cpld_ctrl::cal_iso_sw_t cal_iso = (input == "CAL") ?
+ rhodium_cpld_ctrl::CAL_ISO_CALLOOPBACK :
+ rhodium_cpld_ctrl::CAL_ISO_ISOLATION;
+ const rhodium_cpld_ctrl::rx_sw1_t sw1 = [input]{
+ if (input == "TX/RX")
+ {
+ return rhodium_cpld_ctrl::RX_SW1_FROMTXRXINPUT;
+ }
+ else if (input == "RX2") {
+ return rhodium_cpld_ctrl::RX_SW1_FROMRX2INPUT;
+ }
+ else if (input == "CAL") {
+ return rhodium_cpld_ctrl::RX_SW1_FROMCALLOOPBACK;
+ }
+ else if (input == "TERM") {
+ return rhodium_cpld_ctrl::RX_SW1_ISOLATION;
+ }
+ else {
+ throw uhd::runtime_error("Invalid antenna in _update_rx_input_switches: " + input);
+ }
+ }();
+
+ UHD_LOG_TRACE(unique_id(),
+ "Selected switch values:"
+ " sw10=" << sw10 <<
+ " sw1=" << sw1 <<
+ " cal_iso=" << cal_iso
+ );
+
+ // the TX path may be using TX/RX already, in which case only override sw10 if
+ // we are attempting to use TX/RX
+ if (get_tx_antenna(0) == "TX/RX")
+ {
+ if (input == "TX/RX")
+ {
+ UHD_LOG_TRACE(unique_id(),
+ "Overriding TX antenna to TERM"
+ );
+ // TODO: setting antenna here could cause race conditions
+ set_tx_antenna("TERM", 0);
+
+ UHD_LOG_TRACE(unique_id(),
+ "Setting switch values: sw10=" << sw10
+ );
+ _gpio->set_gpio_out(sw10, SW10_GPIO_MASK);
+ }
+ else {
+ // skip setting sw10, allowing TX to continue using TX/RX
+ UHD_LOG_TRACE(unique_id(),
+ "sw10 setting was not applied because TX antenna is set to TX/RX"
+ );
+ }
+ }
+ else {
+ // TX/RX is not in use, fire away
+ UHD_LOG_TRACE(unique_id(),
+ "Setting switch values: sw10=" << sw10
+ );
+ _gpio->set_gpio_out(sw10, SW10_GPIO_MASK);
+ }
+ _cpld->set_rx_input_switches(sw1, cal_iso);
+}
+
+void rhodium_radio_ctrl_impl::_update_tx_output_switches(
+ const std::string &output
+) {
+ UHD_LOG_TRACE(unique_id(),
+ "Update all TX output related switches. output=" << output
+ );
+ sw10_t sw10;
+ rhodium_cpld_ctrl::tx_sw1_t sw1;
+
+ if (output == "TX/RX")
+ {
+ //Both sw1 and sw10 need to select low/high band
+ if (_map_freq_to_tx_band(get_tx_frequency(0)) == tx_band::TX_BAND_0)
+ {
+ sw1 = rhodium_cpld_ctrl::TX_SW1_TOLOWBAND;
+ sw10 = SW10_FROMTXLOWBAND;
+ }
+ else {
+ sw1 = rhodium_cpld_ctrl::TX_SW1_TOSWITCH2;
+ sw10 = SW10_FROMTXHIGHBAND;
+ }
+ }
+ else if (output == "CAL") {
+ sw1 = rhodium_cpld_ctrl::TX_SW1_TOCALLOOPBACK;
+ sw10 = SW10_ISOLATION;
+ }
+ else if (output == "TERM") {
+ sw1 = rhodium_cpld_ctrl::TX_SW1_ISOLATION;
+ sw10 = SW10_ISOLATION;
+ }
+ else {
+ throw uhd::runtime_error("Invalid antenna in _update_tx_output_switches: " + output);
+ }
+
+ UHD_LOG_TRACE(unique_id(),
+ "Selected switch values: sw1=" << sw1 << " sw10=" << sw10
+ );
+
+ // If RX is on TX/RX, only set sw10 if TX is requesting TX/RX
+ // and override the RX antenna value
+ if (get_rx_antenna(0) == "TX/RX")
+ {
+ if (output == "TX/RX")
+ {
+ UHD_LOG_TRACE(unique_id(),
+ "Overriding RX antenna to TERM"
+ );
+ // TODO: setting antenna here could cause race conditions
+ set_rx_antenna("TERM", 0);
+
+ UHD_LOG_TRACE(unique_id(),
+ "Setting switch values: sw10=" << sw10
+ );
+ _gpio->set_gpio_out(sw10, SW10_GPIO_MASK);
+ }
+ else {
+ // skip setting sw10, allowing RX to continue using TX/RX
+ UHD_LOG_TRACE(unique_id(),
+ "sw10 setting was not applied because RX antenna is set to TX/RX"
+ );
+ }
+ }
+ // If RX is on any other setting, set sw10 normally
+ else {
+ UHD_LOG_TRACE(unique_id(),
+ "Setting switch values: sw10=" << sw10
+ );
+ _gpio->set_gpio_out(sw10, SW10_GPIO_MASK);
+ }
+
+ _cpld->set_tx_output_switches(sw1);
+}
diff --git a/host/lib/usrp/dboard/rhodium/rhodium_radio_ctrl_impl.cpp b/host/lib/usrp/dboard/rhodium/rhodium_radio_ctrl_impl.cpp
new file mode 100644
index 000000000..b9c56a7f0
--- /dev/null
+++ b/host/lib/usrp/dboard/rhodium/rhodium_radio_ctrl_impl.cpp
@@ -0,0 +1,437 @@
+//
+// Copyright 2018 Ettus Research, a National Instruments Company
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+#include "rhodium_radio_ctrl_impl.hpp"
+#include "rhodium_constants.hpp"
+#include <uhdlib/utils/narrow.hpp>
+#include <uhd/utils/log.hpp>
+#include <uhd/rfnoc/node_ctrl_base.hpp>
+#include <uhd/transport/chdr.hpp>
+#include <uhd/utils/algorithm.hpp>
+#include <uhd/utils/math.hpp>
+#include <uhd/types/direction.hpp>
+#include <uhd/types/eeprom.hpp>
+#include <uhd/exception.hpp>
+#include <boost/algorithm/string.hpp>
+#include <boost/make_shared.hpp>
+#include <boost/format.hpp>
+#include <sstream>
+#include <cmath>
+#include <cstdlib>
+
+using namespace uhd;
+using namespace uhd::usrp;
+using namespace uhd::rfnoc;
+using namespace uhd::math::fp_compare;
+
+namespace {
+ constexpr char RX_FE_CONNECTION_LOWBAND[] = "QI";
+ constexpr char RX_FE_CONNECTION_HIGHBAND[] = "IQ";
+ constexpr char TX_FE_CONNECTION_LOWBAND[] = "QI";
+ constexpr char TX_FE_CONNECTION_HIGHBAND[] = "IQ";
+
+ constexpr int DEFAULT_IDENTIFY_DURATION = 5; // seconds
+
+ const fs_path TX_FE_PATH = fs_path("tx_frontends") / 0 / "tune_args";
+ const fs_path RX_FE_PATH = fs_path("rx_frontends") / 0 / "tune_args";
+
+ device_addr_t _get_tune_args(uhd::property_tree::sptr tree, std::string _radio_slot, uhd::direction_t dir)
+ {
+ const auto subtree = tree->subtree(fs_path("dboards") / _radio_slot);
+ const auto tune_arg_path = (dir == RX_DIRECTION) ? RX_FE_PATH : TX_FE_PATH;
+ return subtree->access<device_addr_t>(tune_arg_path).get();
+ }
+}
+
+
+/******************************************************************************
+ * Structors
+ *****************************************************************************/
+UHD_RFNOC_RADIO_BLOCK_CONSTRUCTOR(rhodium_radio_ctrl)
+{
+ UHD_LOG_TRACE(unique_id(), "Entering rhodium_radio_ctrl_impl ctor...");
+ const char radio_slot_name[] = {'A', 'B'};
+ _radio_slot = radio_slot_name[get_block_id().get_block_count()];
+ _rpc_prefix =
+ (_radio_slot == "A") ? "db_0_" : "db_1_";
+ UHD_LOG_TRACE(unique_id(), "Radio slot: " << _radio_slot);
+}
+
+rhodium_radio_ctrl_impl::~rhodium_radio_ctrl_impl()
+{
+ UHD_LOG_TRACE(unique_id(), "rhodium_radio_ctrl_impl::dtor() ");
+}
+
+
+/******************************************************************************
+ * API Calls
+ *****************************************************************************/
+double rhodium_radio_ctrl_impl::set_rate(double /* rate */)
+{
+ // TODO: implement
+ // TODO: set_rate may also need to update the LO since master clock rate
+ // changes also change the lowband LO frequency. (run set_frequency)
+ UHD_LOG_WARNING(unique_id(), "set_rate() called but not implemented");
+ return 0.0;
+}
+
+void rhodium_radio_ctrl_impl::set_tx_antenna(
+ const std::string &ant,
+ const size_t chan
+) {
+ UHD_LOG_TRACE(unique_id(), "set_tx_antenna(ant=" << ant << ", chan=" << chan << ")");
+ UHD_ASSERT_THROW(chan == 0);
+
+ if (!uhd::has(RHODIUM_TX_ANTENNAS, ant)) {
+ throw uhd::value_error(str(
+ boost::format("[%s] Requesting invalid TX antenna value: %s")
+ % unique_id()
+ % ant
+ ));
+ }
+
+ radio_ctrl_impl::set_tx_antenna(ant, chan);
+ _update_tx_output_switches(ant);
+}
+
+void rhodium_radio_ctrl_impl::set_rx_antenna(
+ const std::string &ant,
+ const size_t chan
+) {
+ UHD_LOG_TRACE(unique_id(), "Setting RX antenna to " << ant);
+ UHD_ASSERT_THROW(chan == 0);
+
+ if (!uhd::has(RHODIUM_RX_ANTENNAS, ant)) {
+ throw uhd::value_error(str(
+ boost::format("[%s] Requesting invalid RX antenna value: %s")
+ % unique_id()
+ % ant
+ ));
+ }
+
+ radio_ctrl_impl::set_rx_antenna(ant, chan);
+ _update_rx_input_switches(ant);
+}
+
+void rhodium_radio_ctrl_impl::_set_tx_fe_connection(const std::string &conn)
+{
+ UHD_LOG_TRACE(unique_id(), "set_tx_fe_connection(conn=" << conn << ")");
+
+ if (conn != _tx_fe_connection)
+ {
+ _tx_fe_core->set_mux(conn);
+ _tx_fe_connection = conn;
+ }
+}
+
+void rhodium_radio_ctrl_impl::_set_rx_fe_connection(const std::string &conn)
+{
+ UHD_LOG_TRACE(unique_id(), "set_rx_fe_connection(conn=" << conn << ")");
+
+ if (conn != _tx_fe_connection)
+ {
+ _rx_fe_core->set_fe_connection(conn);
+ _rx_fe_connection = conn;
+ }
+}
+
+std::string rhodium_radio_ctrl_impl::_get_tx_fe_connection() const
+{
+ UHD_LOG_TRACE(unique_id(), "get_tx_fe_connection()");
+
+ return _tx_fe_connection;
+}
+
+std::string rhodium_radio_ctrl_impl::_get_rx_fe_connection() const
+{
+ UHD_LOG_TRACE(unique_id(), "get_rx_fe_connection()");
+
+ return _rx_fe_connection;
+}
+
+double rhodium_radio_ctrl_impl::set_tx_frequency(
+ const double freq,
+ const size_t chan
+) {
+ UHD_LOG_TRACE(unique_id(), "set_tx_frequency(f=" << freq << ", chan=" << chan << ")");
+ UHD_ASSERT_THROW(chan == 0);
+
+ double coerced_target_freq = uhd::clip(freq, RHODIUM_MIN_FREQ, RHODIUM_MAX_FREQ);
+
+ if (freq != coerced_target_freq) {
+ UHD_LOG_DEBUG(unique_id(), "Requested frequency is outside supported range. Coercing to " << coerced_target_freq);
+ }
+
+ const bool is_highband = !_is_tx_lowband(coerced_target_freq);
+
+ const double target_lo_freq = is_highband ?
+ coerced_target_freq : _get_lowband_lo_freq() - coerced_target_freq;
+ const double actual_lo_freq =
+ set_tx_lo_freq(target_lo_freq, RHODIUM_LO1, chan);
+ const double coerced_freq = is_highband ?
+ actual_lo_freq : _get_lowband_lo_freq() - actual_lo_freq;
+ const auto conn = is_highband ?
+ TX_FE_CONNECTION_HIGHBAND : TX_FE_CONNECTION_LOWBAND;
+
+ _set_tx_fe_connection(conn);
+ set_tx_gain(get_tx_gain(chan), 0);
+ radio_ctrl_impl::set_tx_frequency(coerced_freq, chan);
+ _update_tx_freq_switches(coerced_freq);
+
+ return coerced_freq;
+}
+
+double rhodium_radio_ctrl_impl::set_rx_frequency(
+ const double freq,
+ const size_t chan
+) {
+ UHD_LOG_TRACE(unique_id(), "set_rx_frequency(f=" << freq << ", chan=" << chan << ")");
+ UHD_ASSERT_THROW(chan == 0);
+
+ double coerced_target_freq = uhd::clip(freq, RHODIUM_MIN_FREQ, RHODIUM_MAX_FREQ);
+
+ if (freq != coerced_target_freq) {
+ UHD_LOG_DEBUG(unique_id(), "Requested frequency is outside supported range. Coercing to " << coerced_target_freq);
+ }
+
+ const bool is_highband = !_is_rx_lowband(coerced_target_freq);
+
+ const double target_lo_freq = is_highband ?
+ coerced_target_freq : _get_lowband_lo_freq() - coerced_target_freq;
+ const double actual_lo_freq =
+ set_rx_lo_freq(target_lo_freq, RHODIUM_LO1, chan);
+ const double coerced_freq = is_highband ?
+ actual_lo_freq : _get_lowband_lo_freq() - actual_lo_freq;
+ const auto conn = is_highband ?
+ RX_FE_CONNECTION_HIGHBAND : RX_FE_CONNECTION_LOWBAND;
+
+ _set_rx_fe_connection(conn);
+ set_rx_gain(get_rx_gain(chan), 0);
+ radio_ctrl_impl::set_rx_frequency(coerced_freq, chan);
+ _update_rx_freq_switches(coerced_freq);
+
+ return coerced_freq;
+}
+
+double rhodium_radio_ctrl_impl::set_rx_bandwidth(
+ const double bandwidth,
+ const size_t chan
+) {
+ UHD_LOG_TRACE(unique_id(), "set_rx_bandwidth(bandwidth=" << bandwidth << ", chan=" << chan << ")");
+ UHD_ASSERT_THROW(chan == 0);
+
+ return get_rx_bandwidth(chan);
+}
+
+double rhodium_radio_ctrl_impl::set_tx_bandwidth(
+ const double bandwidth,
+ const size_t chan
+) {
+ UHD_LOG_TRACE(unique_id(), "set_tx_bandwidth(bandwidth=" << bandwidth << ", chan=" << chan << ")");
+ UHD_ASSERT_THROW(chan == 0);
+
+ return get_tx_bandwidth(chan);
+}
+
+double rhodium_radio_ctrl_impl::set_tx_gain(
+ const double gain,
+ const size_t chan
+) {
+ UHD_LOG_TRACE(unique_id(), "set_tx_gain(gain=" << gain << ", chan=" << chan << ")");
+ UHD_ASSERT_THROW(chan == 0);
+
+ auto freq = this->get_tx_frequency(chan);
+ auto index = _get_gain_range(TX_DIRECTION).clip(gain);
+
+ auto old_band = _is_tx_lowband(_tx_frequency_at_last_gain_write) ?
+ rhodium_cpld_ctrl::gain_band_t::LOW :
+ rhodium_cpld_ctrl::gain_band_t::HIGH;
+ auto new_band = _is_tx_lowband(freq) ?
+ rhodium_cpld_ctrl::gain_band_t::LOW :
+ rhodium_cpld_ctrl::gain_band_t::HIGH;
+
+ // The CPLD requires a rewrite of the gain control command on a change of lowband or highband
+ if (get_tx_gain(chan) != index or old_band != new_band) {
+ UHD_LOG_TRACE(unique_id(), "Writing new TX gain index: " << index);
+ _cpld->set_gain_index(index, new_band, TX_DIRECTION);
+ _tx_frequency_at_last_gain_write = freq;
+ radio_ctrl_impl::set_tx_gain(index, chan);
+ } else {
+ UHD_LOG_TRACE(unique_id(), "No change in index or band, skipped writing TX gain index: " << index);
+ }
+
+ return index;
+}
+
+double rhodium_radio_ctrl_impl::set_rx_gain(
+ const double gain,
+ const size_t chan
+) {
+ UHD_LOG_TRACE(unique_id(), "set_rx_gain(gain=" << gain << ", chan=" << chan << ")");
+ UHD_ASSERT_THROW(chan == 0);
+
+ auto freq = this->get_rx_frequency(chan);
+ auto index = _get_gain_range(RX_DIRECTION).clip(gain);
+
+ auto old_band = _is_rx_lowband(_rx_frequency_at_last_gain_write) ?
+ rhodium_cpld_ctrl::gain_band_t::LOW :
+ rhodium_cpld_ctrl::gain_band_t::HIGH;
+ auto new_band = _is_rx_lowband(freq) ?
+ rhodium_cpld_ctrl::gain_band_t::LOW :
+ rhodium_cpld_ctrl::gain_band_t::HIGH;
+
+ // The CPLD requires a rewrite of the gain control command on a change of lowband or highband
+ if (get_rx_gain(chan) != index or old_band != new_band) {
+ UHD_LOG_TRACE(unique_id(), "Writing new RX gain index: " << index);
+ _cpld->set_gain_index(index, new_band, RX_DIRECTION);
+ _rx_frequency_at_last_gain_write = freq;
+ radio_ctrl_impl::set_rx_gain(index, chan);
+ } else {
+ UHD_LOG_TRACE(unique_id(), "No change in index or band, skipped writing RX gain index: " << index);
+ }
+
+ return index;
+}
+
+uhd::gain_range_t rhodium_radio_ctrl_impl::_get_gain_range(direction_t dir)
+{
+ if (dir == RX_DIRECTION) {
+ return gain_range_t(RX_MIN_GAIN, RX_MAX_GAIN, RX_GAIN_STEP);
+ } else if (dir == TX_DIRECTION) {
+ return gain_range_t(TX_MIN_GAIN, TX_MAX_GAIN, TX_GAIN_STEP);
+ } else {
+ UHD_THROW_INVALID_CODE_PATH();
+ }
+}
+
+bool rhodium_radio_ctrl_impl::_get_spur_dodging_enabled(uhd::direction_t dir) const
+{
+ auto dict = _get_tune_args(_tree, _radio_slot, dir);
+
+ // get the current tune_arg for spur_dodging
+ // if the tune_arg doesn't exist, get the radio block argument instead
+ std::string spur_dodging_arg = dict.cast<std::string>(
+ "spur_dodging",
+ (_tree->access<std::string>(get_arg_path("spur_dodging") / "value").get()));
+
+ if (spur_dodging_arg == "enabled")
+ {
+ return true;
+ }
+ else if (spur_dodging_arg == "disabled") {
+ return false;
+ }
+ else {
+ throw uhd::value_error(
+ str(boost::format("Invalid spur_dodging argument: %s Valid options are [enabled, disabled]")
+ % spur_dodging_arg));
+ }
+}
+
+double rhodium_radio_ctrl_impl::_get_spur_dodging_threshold(uhd::direction_t dir) const
+{
+ auto dict = _get_tune_args(_tree, _radio_slot, dir);
+
+ // get the current tune_arg for spur_dodging_threshold
+ // if the tune_arg doesn't exist, get the radio block argument instead
+ return dict.cast(
+ "spur_dodging_threshold",
+ _tree->access<double>(get_arg_path("spur_dodging_threshold") / "value").get());
+}
+
+size_t rhodium_radio_ctrl_impl::get_chan_from_dboard_fe(
+ const std::string &fe, const direction_t /* dir */
+) {
+ UHD_ASSERT_THROW(boost::lexical_cast<size_t>(fe) == 0);
+ return 0;
+}
+
+std::string rhodium_radio_ctrl_impl::get_dboard_fe_from_chan(
+ const size_t chan,
+ const direction_t /* dir */
+) {
+ UHD_ASSERT_THROW(chan == 0);
+ return "0";
+}
+
+void rhodium_radio_ctrl_impl::set_rpc_client(
+ uhd::rpc_client::sptr rpcc,
+ const uhd::device_addr_t &block_args
+) {
+ _rpcc = rpcc;
+ _block_args = block_args;
+
+ // Get and verify the MCR before _init_peripherals, which will use this value
+ // Note: MCR gets set during the init() call (prior to this), which takes
+ // in arguments from the device args. So if block_args contains a
+ // master_clock_rate key, then it should better be whatever the device is
+ // configured to do.
+ _master_clock_rate = _rpcc->request_with_token<double>(_rpc_prefix + "get_master_clock_rate");
+ if (block_args.cast<double>("master_clock_rate", _master_clock_rate)
+ != _master_clock_rate) {
+ throw uhd::runtime_error(str(
+ boost::format("Master clock rate mismatch. Device returns %f MHz, "
+ "but should have been %f MHz.")
+ % (_master_clock_rate / 1e6)
+ % (block_args.cast<double>(
+ "master_clock_rate", _master_clock_rate) / 1e6)
+ ));
+ }
+ UHD_LOG_DEBUG(unique_id(),
+ "Master Clock Rate is: " << (_master_clock_rate / 1e6) << " MHz.");
+ radio_ctrl_impl::set_rate(_master_clock_rate);
+
+ UHD_LOG_TRACE(unique_id(), "Checking for existence of Rhodium DB in slot " << _radio_slot);
+ const auto dboard_info = _rpcc->request<std::vector<std::map<std::string, std::string>>>("get_dboard_info");
+
+ // There is a bug that if only one DB is plugged into slot B the vector
+ // will only have 1 element but not be correlated to slot B at all.
+ // For now, we assume a 1 element array means the DB is in slot A.
+ if (dboard_info.size() <= get_block_id().get_block_count())
+ {
+ UHD_LOG_DEBUG(unique_id(), "No DB detected in slot " << _radio_slot);
+ // Name and master clock rate are needed for RFNoC init, so set the
+ // name now and let this function continue to set the MCR
+ _tree->subtree(fs_path("dboards") / _radio_slot / "tx_frontends" / "0")
+ ->create<std::string>("name").set("Unknown");
+ _tree->subtree(fs_path("dboards") / _radio_slot / "rx_frontends" / "0")
+ ->create<std::string>("name").set("Unknown");
+ }
+ else {
+ UHD_LOG_DEBUG(unique_id(),
+ "Rhodium DB detected in slot " <<
+ _radio_slot <<
+ ". Serial: " <<
+ dboard_info.at(get_block_id().get_block_count()).at("serial"));
+ _init_defaults();
+ _init_peripherals();
+ _init_prop_tree();
+ }
+
+ if (block_args.has_key("identify")) {
+ const std::string identify_val = block_args.get("identify");
+ int identify_duration = std::atoi(identify_val.c_str());
+ if (identify_duration == 0) {
+ identify_duration = DEFAULT_IDENTIFY_DURATION;
+ }
+ // TODO: Update this when LED control is added
+ //UHD_LOG_INFO(unique_id(),
+ // "Running LED identification process for " << identify_duration
+ // << " seconds.");
+ //_identify_with_leds(identify_duration);
+ }
+}
+
+bool rhodium_radio_ctrl_impl::get_lo_lock_status(
+ const direction_t dir
+) const
+{
+ return
+ ((dir == RX_DIRECTION) or _tx_lo->get_lock_status()) and
+ ((dir == TX_DIRECTION) or _rx_lo->get_lock_status());
+}
+
+UHD_RFNOC_BLOCK_REGISTER(rhodium_radio_ctrl, "RhodiumRadio");
diff --git a/host/lib/usrp/dboard/rhodium/rhodium_radio_ctrl_impl.hpp b/host/lib/usrp/dboard/rhodium/rhodium_radio_ctrl_impl.hpp
new file mode 100644
index 000000000..4f4dd925c
--- /dev/null
+++ b/host/lib/usrp/dboard/rhodium/rhodium_radio_ctrl_impl.hpp
@@ -0,0 +1,345 @@
+//
+// Copyright 2018 Ettus Research, a National Instruments Company
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+#ifndef INCLUDED_LIBUHD_RFNOC_RHODIUM_RADIO_CTRL_IMPL_HPP
+#define INCLUDED_LIBUHD_RFNOC_RHODIUM_RADIO_CTRL_IMPL_HPP
+
+#include "rhodium_cpld_ctrl.hpp"
+#include "rhodium_cpld_regs.hpp"
+#include <uhdlib/usrp/common/lmx2592.hpp>
+#include <uhdlib/usrp/cores/gpio_atr_3000.hpp>
+#include <uhdlib/rfnoc/rpc_block_ctrl.hpp>
+#include <uhdlib/rfnoc/radio_ctrl_impl.hpp>
+#include <uhdlib/usrp/cores/rx_frontend_core_3000.hpp>
+#include <uhdlib/usrp/cores/tx_frontend_core_200.hpp>
+#include <uhd/types/serial.hpp>
+#include <uhd/usrp/dboard_manager.hpp>
+#include <uhd/usrp/gpio_defs.hpp>
+#include <mutex>
+
+namespace uhd {
+ namespace rfnoc {
+
+/*! \brief Provide access to an Rhodium radio.
+ */
+class rhodium_radio_ctrl_impl : public radio_ctrl_impl, public rpc_block_ctrl
+{
+public:
+ typedef boost::shared_ptr<rhodium_radio_ctrl_impl> sptr;
+
+ //! Frequency bands for RX. Bands are a function of the analog filter banks
+ enum class rx_band {
+ RX_BAND_INVALID,
+ RX_BAND_0,
+ RX_BAND_1,
+ RX_BAND_2,
+ RX_BAND_3,
+ RX_BAND_4,
+ RX_BAND_5,
+ RX_BAND_6,
+ RX_BAND_7
+ };
+
+ //! Frequency bands for TX. Bands are a function of the analog filter banks
+ enum class tx_band {
+ TX_BAND_INVALID,
+ TX_BAND_0,
+ TX_BAND_1,
+ TX_BAND_2,
+ TX_BAND_3,
+ TX_BAND_4,
+ TX_BAND_5,
+ TX_BAND_6,
+ TX_BAND_7
+ };
+
+ enum sw10_t {
+ SW10_FROMTXLOWBAND = 0,
+ SW10_FROMTXHIGHBAND = 1,
+ SW10_ISOLATION = 2,
+ SW10_TORX = 3
+ };
+
+ /************************************************************************
+ * Structors
+ ***********************************************************************/
+ UHD_RFNOC_RADIO_BLOCK_CONSTRUCTOR_DECL(rhodium_radio_ctrl)
+ virtual ~rhodium_radio_ctrl_impl();
+
+ /************************************************************************
+ * API calls
+ ***********************************************************************/
+ // Note: We use the cached values in radio_ctrl_impl, so most getters are
+ // not reimplemented here
+ double set_rate(const double rate);
+
+ void set_tx_antenna(const std::string &ant, const size_t chan);
+ void set_rx_antenna(const std::string &ant, const size_t chan);
+
+ double set_tx_frequency(const double freq, const size_t chan);
+ double set_rx_frequency(const double freq, const size_t chan);
+
+ double set_tx_bandwidth(const double bandwidth, const size_t chan);
+ double set_rx_bandwidth(const double bandwidth, const size_t chan);
+
+ double set_tx_gain(const double gain, const size_t chan);
+ double set_rx_gain(const double gain, const size_t chan);
+
+ // LO Property Getters
+ std::vector<std::string> get_tx_lo_names(const size_t chan);
+ std::vector<std::string> get_rx_lo_names(const size_t chan);
+ std::vector<std::string> get_tx_lo_sources(const std::string& name, const size_t chan);
+ std::vector<std::string> get_rx_lo_sources(const std::string& name, const size_t chan);
+ freq_range_t get_tx_lo_freq_range(const std::string& name, const size_t chan);
+ freq_range_t get_rx_lo_freq_range(const std::string& name, const size_t chan);
+
+ // LO Frequency Control
+ double set_tx_lo_freq(const double freq, const std::string& name, const size_t chan);
+ double set_rx_lo_freq(const double freq, const std::string& name, const size_t chan);
+ double get_tx_lo_freq(const std::string& name, const size_t chan);
+ double get_rx_lo_freq(const std::string& name, const size_t chan);
+
+ // LO Source Control
+ void set_tx_lo_source(const std::string& src, const std::string& name, const size_t chan);
+ void set_rx_lo_source(const std::string& src, const std::string& name, const size_t chan);
+ const std::string get_tx_lo_source(const std::string& name, const size_t chan);
+ const std::string get_rx_lo_source(const std::string& name, const size_t chan);
+
+ // LO Export Control
+ void set_tx_lo_export_enabled(const bool enabled, const std::string& name, const size_t chan);
+ void set_rx_lo_export_enabled(const bool enabled, const std::string& name, const size_t chan);
+ bool get_tx_lo_export_enabled(const std::string& name, const size_t chan);
+ bool get_rx_lo_export_enabled(const std::string& name, const size_t chan);
+
+ // LO Gain Control
+
+ //! Set the external gain for a TX LO
+ // Out of range values will be coerced
+ double set_tx_lo_gain(const double gain, const std::string &name, const size_t chan);
+
+ //! Set the external gain for an RX LO
+ // Out of range values will be coerced
+ double set_rx_lo_gain(const double gain, const std::string &name, const size_t chan);
+
+ double get_tx_lo_gain(const std::string &name, const size_t chan);
+ double get_rx_lo_gain(const std::string &name, const size_t chan);
+
+ // LO Output Power Control
+
+ //! Set the output power setting of a TX LO
+ // Out of range values will be coerced
+ double set_tx_lo_power(const double power, const std::string &name, const size_t chan);
+
+ //! Set the output power setting of a RX LO
+ // Out of range values will be coerced
+ double set_rx_lo_power(const double power, const std::string &name, const size_t chan);
+
+ double get_tx_lo_power(const std::string &name, const size_t chan);
+ double get_rx_lo_power(const std::string &name, const size_t chan);
+
+ size_t get_chan_from_dboard_fe(const std::string &fe, const direction_t dir);
+ std::string get_dboard_fe_from_chan(const size_t chan, const direction_t dir);
+
+ void set_rpc_client(
+ uhd::rpc_client::sptr rpcc,
+ const uhd::device_addr_t &block_args
+ );
+
+private:
+ /**************************************************************************
+ * Helpers
+ *************************************************************************/
+ //! Initialize all the peripherals connected to this block
+ void _init_peripherals();
+
+ //! Set state of this class to sensible defaults
+ void _init_defaults();
+
+ //! Init a subtree for the RF frontends
+ void _init_frontend_subtree(
+ uhd::property_tree::sptr subtree,
+ const size_t chan_idx
+ );
+
+ //! Initialize property tree
+ void _init_prop_tree();
+
+ //! Discover and initialize any mpm sensors
+ void _init_mpm_sensors(
+ const direction_t dir,
+ const size_t chan_idx
+ );
+
+ //! Get the frequency range for an LO
+ freq_range_t _get_lo_freq_range(const std::string &name) const;
+
+ //! Get the current lowband intermediate frequency
+ double _get_lowband_lo_freq() const;
+
+ //! Configure LO1's export
+ void _set_lo1_export_enabled(
+ const bool enabled,
+ const direction_t dir
+ );
+
+ //! Configure LO1's output power
+ // Out of range values will be coerced to [0-63]
+ double _set_lo1_power(
+ const double power,
+ const direction_t dir
+ );
+
+ //! Map a frequency in Hz to an rx_band value. Will return
+ // rx_band::INVALID_BAND if the frequency is out of range.
+ static rx_band _map_freq_to_rx_band(const double freq);
+ //! Map a frequency in Hz to an tx_band value. Will return
+ // tx_band::INVALID_BAND if the frequency is out of range.
+ static tx_band _map_freq_to_tx_band(const double freq);
+
+ //! Return if the given rx frequency is in lowband
+ // NOTE: Returns false if frequency is out of Rh's rx frequency range
+ static bool _is_rx_lowband(const double freq);
+ //! Return if the given tx frequency is in lowband
+ // NOTE: Returns false if frequency is out of Rh's tx frequency range
+ static bool _is_tx_lowband(const double freq);
+
+ //! Return the gain range for dir
+ static uhd::gain_range_t _get_gain_range(const direction_t dir);
+ //! Return the gain range of the LMX LO
+ static uhd::gain_range_t _get_lo_gain_range();
+ //! Return the power setting range of the LMX LO
+ static uhd::gain_range_t _get_lo_power_range();
+
+ //! Lookup the LO DSA setting from LO frequency
+ int _get_lo_dsa_setting(const double freq);
+
+ //! Lookup the LO output power setting from LO frequency
+ unsigned int _get_lo_power_setting(const double freq);
+
+ bool _get_spur_dodging_enabled(const uhd::direction_t dir) const;
+ double _get_spur_dodging_threshold(const uhd::direction_t dir) const;
+
+ /**************************************************************************
+ * Sensors
+ *************************************************************************/
+ //! Return LO lock status. Factors in current band (low/high) and
+ // direction (TX/RX)
+ bool get_lo_lock_status(const direction_t dir) const;
+
+ /**************************************************************************
+ * Frontend Controls
+ *************************************************************************/
+
+ void _set_tx_fe_connection(const std::string &conn);
+ void _set_rx_fe_connection(const std::string &conn);
+ std::string _get_tx_fe_connection() const;
+ std::string _get_rx_fe_connection() const;
+
+ /**************************************************************************
+ * CPLD Controls (implemented in rhodium_radio_ctrl_cpld.cpp)
+ *************************************************************************/
+ void _update_rx_freq_switches(
+ const double freq
+ );
+
+ void _update_tx_freq_switches(
+ const double freq
+ );
+
+ void _update_rx_input_switches(
+ const std::string &input
+ );
+
+ void _update_tx_output_switches(
+ const std::string &output
+ );
+
+ /**************************************************************************
+ * Private attributes
+ *************************************************************************/
+ //! Locks access to setter APIs
+ std::mutex _set_lock;
+
+ //! Letter representation of the radio we're currently running
+ std::string _radio_slot;
+
+ //! Prepended for all dboard RPC calls
+ std::string _rpc_prefix;
+
+ //! Additional block args; gets set during set_rpc_client()
+ uhd::device_addr_t _block_args;
+
+ //! Reference to the RPC client
+ uhd::rpc_client::sptr _rpcc;
+
+ //! Reference to the SPI core
+ uhd::spi_iface::sptr _spi;
+
+ //! Reference to the TX LO
+ lmx2592_iface::sptr _tx_lo;
+
+ //! Reference to the RX LO
+ lmx2592_iface::sptr _rx_lo;
+
+ //! Reference to the CPLD controls. Even if there's multiple radios,
+ // there's only one CPLD control.
+ std::shared_ptr<rhodium_cpld_ctrl> _cpld;
+
+ //! ATR controls. These control the external DSA and the AD9371 gain
+ // up/down bits. They do *not* control the ATR state of the CPLD, the
+ // tx/rx run states are hooked up directly to the CPLD.
+ //
+ // Every radio channel gets its own ATR state register.
+ usrp::gpio_atr::gpio_atr_3000::sptr _gpio;
+
+ //! Front panel GPIO controller. Note that only one radio block per
+ // module can be the FP-GPIO master.
+ usrp::gpio_atr::gpio_atr_3000::sptr _fp_gpio;
+
+ //! One DSP core per channel
+ rx_frontend_core_3000::sptr _rx_fe_core;
+ tx_frontend_core_200::sptr _tx_fe_core;
+
+ //! Sampling rate, and also ref clock frequency for the lowband LOs.
+ double _master_clock_rate = 1.0;
+ //! Saved frontend connection for DSP core
+ std::string _rx_fe_connection;
+ std::string _tx_fe_connection;
+ //! Desired RF frequency
+ std::map<direction_t,double> _desired_rf_freq = { {RX_DIRECTION, 2.44e9}, {TX_DIRECTION, 2.44e9} };
+ //! Frequency at which gain setting was last applied. The CPLD requires a new gain
+ // control write when switching between lowband and highband frequencies, so save
+ // the frequency when sending a gain control command.
+ double _tx_frequency_at_last_gain_write = 0.0;
+ double _rx_frequency_at_last_gain_write = 0.0;
+ //! LO gain
+ double _lo_rx_gain = 0.0;
+ double _lo_tx_gain = 0.0;
+ //! LO output power
+ double _lo_rx_power = 0.0;
+ double _lo_tx_power = 0.0;
+ //! Gain profile
+ std::map<direction_t,std::string> _gain_profile = { {RX_DIRECTION, "default"}, {TX_DIRECTION, "default"} };
+
+ //! LO source
+ std::string _rx_lo_source = "internal";
+ std::string _tx_lo_source = "internal";
+
+ //! LO export enabled
+ bool _rx_lo_exported = false;
+ bool _tx_lo_exported = false;
+
+ //! LO state frequency
+ double _rx_lo_freq = 0.0;
+ double _tx_lo_freq = 0.0;
+
+}; /* class radio_ctrl_impl */
+
+}} /* namespace uhd::rfnoc */
+
+#endif /* INCLUDED_LIBUHD_RFNOC_RHODIUM_RADIO_CTRL_IMPL_HPP */
+// vim: sw=4 et:
+
diff --git a/host/lib/usrp/dboard/rhodium/rhodium_radio_ctrl_init.cpp b/host/lib/usrp/dboard/rhodium/rhodium_radio_ctrl_init.cpp
new file mode 100644
index 000000000..c2c2002eb
--- /dev/null
+++ b/host/lib/usrp/dboard/rhodium/rhodium_radio_ctrl_init.cpp
@@ -0,0 +1,710 @@
+//
+// Copyright 2018 Ettus Research, a National Instruments Company
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+#include "rhodium_radio_ctrl_impl.hpp"
+#include "rhodium_constants.hpp"
+#include <uhdlib/usrp/cores/spi_core_3000.hpp>
+#include <uhd/utils/log.hpp>
+#include <uhd/utils/algorithm.hpp>
+#include <uhd/types/eeprom.hpp>
+#include <uhd/types/sensors.hpp>
+#include <uhd/transport/chdr.hpp>
+#include <vector>
+#include <string>
+
+using namespace uhd;
+using namespace uhd::rfnoc;
+
+namespace {
+ enum slave_select_t {
+ SEN_CPLD = 8,
+ SEN_TX_LO = 1,
+ SEN_RX_LO = 2,
+ SEN_LO_DIST = 4 /* Unused */
+ };
+
+ constexpr uint32_t TX_FE_BASE = 224;
+ constexpr uint32_t RX_FE_BASE = 232;
+
+ constexpr double RHODIUM_DEFAULT_FREQ = 2.5e9; // Hz
+ // An invalid default index ensures that set gain will apply settings
+ // the first time it is called
+ constexpr double RHODIUM_DEFAULT_INVALID_GAIN = -1; // gain index
+ constexpr double RHODIUM_DEFAULT_GAIN = 0; // gain index
+ constexpr double RHODIUM_DEFAULT_LO_GAIN = 30; // gain index
+ constexpr char RHODIUM_DEFAULT_RX_ANTENNA[] = "RX2";
+ constexpr char RHODIUM_DEFAULT_TX_ANTENNA[] = "TX/RX";
+ constexpr double RHODIUM_DEFAULT_BANDWIDTH = 250e6; // Hz
+
+ //! Rhodium gain profile options
+ const std::vector<std::string> RHODIUM_GP_OPTIONS = {
+ "default"
+ };
+
+ //! Returns the SPI config used by the CPLD
+ spi_config_t _get_cpld_spi_config() {
+ spi_config_t spi_config;
+ spi_config.use_custom_divider = true;
+ spi_config.divider = 10;
+ spi_config.mosi_edge = spi_config_t::EDGE_RISE;
+ spi_config.miso_edge = spi_config_t::EDGE_FALL;
+
+ return spi_config;
+ }
+
+ //! Returns the SPI config used by the TX LO
+ spi_config_t _get_tx_lo_spi_config() {
+ spi_config_t spi_config;
+ spi_config.use_custom_divider = true;
+ spi_config.divider = 10;
+ spi_config.mosi_edge = spi_config_t::EDGE_RISE;
+ spi_config.miso_edge = spi_config_t::EDGE_FALL;
+
+ return spi_config;
+ }
+
+ //! Returns the SPI config used by the RX LO
+ spi_config_t _get_rx_lo_spi_config() {
+ spi_config_t spi_config;
+ spi_config.use_custom_divider = true;
+ spi_config.divider = 10;
+ spi_config.mosi_edge = spi_config_t::EDGE_RISE;
+ spi_config.miso_edge = spi_config_t::EDGE_FALL;
+
+ return spi_config;
+ }
+
+ std::function<void(uint32_t)> _generate_write_spi(
+ uhd::spi_iface::sptr spi,
+ slave_select_t slave,
+ spi_config_t config
+ ) {
+ return [spi, slave, config](const uint32_t transaction) {
+ spi->write_spi(slave, config, transaction, 24);
+ };
+ }
+
+ std::function<uint32_t(uint32_t)> _generate_read_spi(
+ uhd::spi_iface::sptr spi,
+ slave_select_t slave,
+ spi_config_t config
+ ) {
+ return [spi, slave, config](const uint32_t transaction) {
+ return spi->read_spi(slave, config, transaction, 24);
+ };
+ }
+}
+
+void rhodium_radio_ctrl_impl::_init_defaults()
+{
+ UHD_LOG_TRACE(unique_id(), "Initializing defaults...");
+ const size_t num_rx_chans = get_output_ports().size();
+ const size_t num_tx_chans = get_input_ports().size();
+
+ UHD_LOG_TRACE(unique_id(),
+ "Num TX chans: " << num_tx_chans
+ << " Num RX chans: " << num_rx_chans);
+
+ for (size_t chan = 0; chan < num_rx_chans; chan++) {
+ radio_ctrl_impl::set_rx_frequency(RHODIUM_DEFAULT_FREQ, chan);
+ radio_ctrl_impl::set_rx_gain(RHODIUM_DEFAULT_INVALID_GAIN, chan);
+ radio_ctrl_impl::set_rx_antenna(RHODIUM_DEFAULT_RX_ANTENNA, chan);
+ radio_ctrl_impl::set_rx_bandwidth(RHODIUM_DEFAULT_BANDWIDTH, chan);
+ }
+
+ for (size_t chan = 0; chan < num_tx_chans; chan++) {
+ radio_ctrl_impl::set_tx_frequency(RHODIUM_DEFAULT_FREQ, chan);
+ radio_ctrl_impl::set_tx_gain(RHODIUM_DEFAULT_INVALID_GAIN, chan);
+ radio_ctrl_impl::set_tx_antenna(RHODIUM_DEFAULT_TX_ANTENNA, chan);
+ radio_ctrl_impl::set_rx_bandwidth(RHODIUM_DEFAULT_BANDWIDTH, chan);
+ }
+
+ /** Update default SPP (overwrites the default value from the XML file) **/
+ const size_t max_bytes_header =
+ uhd::transport::vrt::chdr::max_if_hdr_words64 * sizeof(uint64_t);
+ const size_t default_spp =
+ (_tree->access<size_t>("mtu/recv").get() - max_bytes_header)
+ / (2 * sizeof(int16_t));
+ UHD_LOG_DEBUG(unique_id(),
+ "Setting default spp to " << default_spp);
+ _tree->access<int>(get_arg_path("spp") / "value").set(default_spp);
+}
+
+void rhodium_radio_ctrl_impl::_init_peripherals()
+{
+ UHD_LOG_TRACE(unique_id(), "Initializing peripherals...");
+
+ UHD_LOG_TRACE(unique_id(), "Initializing SPI core...");
+ _spi = spi_core_3000::make(_get_ctrl(0),
+ regs::sr_addr(regs::SPI),
+ regs::rb_addr(regs::RB_SPI)
+ );
+
+ UHD_LOG_TRACE(unique_id(), "Initializing CPLD...");
+ _cpld = std::make_shared<rhodium_cpld_ctrl>(
+ _generate_write_spi(this->_spi, SEN_CPLD, _get_cpld_spi_config()),
+ _generate_read_spi(this->_spi, SEN_CPLD, _get_cpld_spi_config()));
+
+ UHD_LOG_TRACE(unique_id(), "Writing initial gain values...");
+ set_tx_gain(RHODIUM_DEFAULT_GAIN, 0);
+ set_tx_lo_gain(RHODIUM_DEFAULT_LO_GAIN, RHODIUM_LO1, 0);
+ set_rx_gain(RHODIUM_DEFAULT_GAIN, 0);
+ set_rx_lo_gain(RHODIUM_DEFAULT_LO_GAIN, RHODIUM_LO1, 0);
+
+ UHD_LOG_TRACE(unique_id(), "Initializing TX LO...");
+ _tx_lo = lmx2592_iface::make(
+ _generate_write_spi(this->_spi, SEN_TX_LO, _get_tx_lo_spi_config()),
+ _generate_read_spi(this->_spi, SEN_TX_LO, _get_tx_lo_spi_config()));
+
+ UHD_LOG_TRACE(unique_id(), "Writing initial TX LO state...");
+ _tx_lo->set_reference_frequency(RHODIUM_LO1_REF_FREQ);
+ _tx_lo->set_mash_order(lmx2592_iface::mash_order_t::THIRD);
+
+ UHD_LOG_TRACE(unique_id(), "Initializing RX LO...");
+ _rx_lo = lmx2592_iface::make(
+ _generate_write_spi(this->_spi, SEN_RX_LO, _get_rx_lo_spi_config()),
+ _generate_read_spi(this->_spi, SEN_RX_LO, _get_rx_lo_spi_config()));
+
+ UHD_LOG_TRACE(unique_id(), "Writing initial RX LO state...");
+ _rx_lo->set_reference_frequency(RHODIUM_LO1_REF_FREQ);
+ _rx_lo->set_mash_order(lmx2592_iface::mash_order_t::THIRD);
+
+ UHD_LOG_TRACE(unique_id(), "Initializing GPIOs...");
+ _gpio =
+ usrp::gpio_atr::gpio_atr_3000::make(
+ _get_ctrl(0),
+ regs::sr_addr(regs::GPIO),
+ regs::rb_addr(regs::RB_DB_GPIO)
+ );
+ _gpio->set_atr_mode(
+ usrp::gpio_atr::MODE_GPIO, // Disable ATR mode
+ usrp::gpio_atr::gpio_atr_3000::MASK_SET_ALL
+ );
+ _gpio->set_gpio_ddr(
+ usrp::gpio_atr::DDR_OUTPUT, // Make all GPIOs outputs
+ usrp::gpio_atr::gpio_atr_3000::MASK_SET_ALL
+ );
+
+ // TODO: put this in the right spot
+ UHD_LOG_TRACE(unique_id(), "Setting Switch 10 to 0x1");
+ _gpio->set_gpio_out(0x1, 0x3);
+
+ _rx_fe_core = rx_frontend_core_3000::make(_get_ctrl(0), regs::sr_addr(RX_FE_BASE));
+ _rx_fe_core->set_adc_rate(_master_clock_rate);
+ _rx_fe_core->set_dc_offset(rx_frontend_core_3000::DEFAULT_DC_OFFSET_VALUE);
+ _rx_fe_core->set_dc_offset_auto(rx_frontend_core_3000::DEFAULT_DC_OFFSET_ENABLE);
+ _rx_fe_core->populate_subtree(_tree->subtree(_root_path / "rx_fe_corrections" / 0));
+
+ _tx_fe_core = tx_frontend_core_200::make(_get_ctrl(0), regs::sr_addr(TX_FE_BASE));
+ _tx_fe_core->set_dc_offset(tx_frontend_core_200::DEFAULT_DC_OFFSET_VALUE);
+ _tx_fe_core->set_iq_balance(tx_frontend_core_200::DEFAULT_IQ_BALANCE_VALUE);
+ _tx_fe_core->populate_subtree(_tree->subtree(_root_path / "tx_fe_corrections" / 0));
+}
+
+void rhodium_radio_ctrl_impl::_init_frontend_subtree(
+ uhd::property_tree::sptr subtree,
+ const size_t chan_idx
+) {
+ const fs_path tx_fe_path = fs_path("tx_frontends") / chan_idx;
+ const fs_path rx_fe_path = fs_path("rx_frontends") / chan_idx;
+ UHD_LOG_TRACE(unique_id(),
+ "Adding non-RFNoC block properties for channel " << chan_idx <<
+ " to prop tree path " << tx_fe_path << " and " << rx_fe_path);
+ // TX Standard attributes
+ subtree->create<std::string>(tx_fe_path / "name")
+ .set(str(boost::format("Rhodium")))
+ ;
+ subtree->create<std::string>(tx_fe_path / "connection")
+ .add_coerced_subscriber([this](const std::string& conn){
+ this->_set_tx_fe_connection(conn);
+ })
+ .set_publisher([this](){
+ return this->_get_tx_fe_connection();
+ })
+ ;
+ subtree->create<device_addr_t>(tx_fe_path / "tune_args")
+ .set(device_addr_t())
+ ;
+ // RX Standard attributes
+ subtree->create<std::string>(rx_fe_path / "name")
+ .set(str(boost::format("Rhodium")))
+ ;
+ subtree->create<std::string>(rx_fe_path / "connection")
+ .add_coerced_subscriber([this](const std::string& conn){
+ this->_set_rx_fe_connection(conn);
+ })
+ .set_publisher([this](){
+ return this->_get_rx_fe_connection();
+ })
+ ;
+ subtree->create<device_addr_t>(rx_fe_path / "tune_args")
+ .set(device_addr_t())
+ ;
+ // TX Antenna
+ subtree->create<std::string>(tx_fe_path / "antenna" / "value")
+ .add_coerced_subscriber([this, chan_idx](const std::string &ant){
+ this->set_tx_antenna(ant, chan_idx);
+ })
+ .set_publisher([this, chan_idx](){
+ return this->get_tx_antenna(chan_idx);
+ })
+ ;
+ subtree->create<std::vector<std::string>>(tx_fe_path / "antenna" / "options")
+ .set({RHODIUM_DEFAULT_TX_ANTENNA})
+ .add_coerced_subscriber([](const std::vector<std::string> &){
+ throw uhd::runtime_error(
+ "Attempting to update antenna options!");
+ })
+ ;
+ // RX Antenna
+ subtree->create<std::string>(rx_fe_path / "antenna" / "value")
+ .add_coerced_subscriber([this, chan_idx](const std::string &ant){
+ this->set_rx_antenna(ant, chan_idx);
+ })
+ .set_publisher([this, chan_idx](){
+ return this->get_rx_antenna(chan_idx);
+ })
+ ;
+ subtree->create<std::vector<std::string>>(rx_fe_path / "antenna" / "options")
+ .set(RHODIUM_RX_ANTENNAS)
+ .add_coerced_subscriber([](const std::vector<std::string> &){
+ throw uhd::runtime_error(
+ "Attempting to update antenna options!");
+ })
+ ;
+ // TX frequency
+ subtree->create<double>(tx_fe_path / "freq" / "value")
+ .set_coercer([this, chan_idx](const double freq){
+ return this->set_tx_frequency(freq, chan_idx);
+ })
+ .set_publisher([this, chan_idx](){
+ return this->get_tx_frequency(chan_idx);
+ })
+ ;
+ subtree->create<meta_range_t>(tx_fe_path / "freq" / "range")
+ .set(meta_range_t(RHODIUM_MIN_FREQ, RHODIUM_MAX_FREQ, 1.0))
+ .add_coerced_subscriber([](const meta_range_t &){
+ throw uhd::runtime_error(
+ "Attempting to update freq range!");
+ })
+ ;
+ // RX frequency
+ subtree->create<double>(rx_fe_path / "freq" / "value")
+ .set_coercer([this, chan_idx](const double freq){
+ return this->set_rx_frequency(freq, chan_idx);
+ })
+ .set_publisher([this, chan_idx](){
+ return this->get_rx_frequency(chan_idx);
+ })
+ ;
+ subtree->create<meta_range_t>(rx_fe_path / "freq" / "range")
+ .set(meta_range_t(RHODIUM_MIN_FREQ, RHODIUM_MAX_FREQ, 1.0))
+ .add_coerced_subscriber([](const meta_range_t &){
+ throw uhd::runtime_error(
+ "Attempting to update freq range!");
+ })
+ ;
+ // TX bandwidth
+ subtree->create<double>(tx_fe_path / "bandwidth" / "value")
+ .set_coercer([this, chan_idx](const double bw){
+ return this->set_tx_bandwidth(bw, chan_idx);
+ })
+ .set_publisher([this, chan_idx](){
+ return this->get_tx_bandwidth(chan_idx);
+ })
+ ;
+ subtree->create<meta_range_t>(tx_fe_path / "bandwidth" / "range")
+ .set(meta_range_t(0.0, 0.0, 0.0)) // FIXME
+ .add_coerced_subscriber([](const meta_range_t &){
+ throw uhd::runtime_error(
+ "Attempting to update bandwidth range!");
+ })
+ ;
+ // RX bandwidth
+ subtree->create<double>(rx_fe_path / "bandwidth" / "value")
+ .set_coercer([this, chan_idx](const double bw){
+ return this->set_rx_bandwidth(bw, chan_idx);
+ })
+ .set_publisher([this, chan_idx](){
+ return this->get_rx_bandwidth(chan_idx);
+ })
+ ;
+ subtree->create<meta_range_t>(rx_fe_path / "bandwidth" / "range")
+ .set(meta_range_t(0.0, 0.0, 0.0)) // FIXME
+ .add_coerced_subscriber([](const meta_range_t &){
+ throw uhd::runtime_error(
+ "Attempting to update bandwidth range!");
+ })
+ ;
+ // TX gains
+ subtree->create<double>(tx_fe_path / "gains" / "all" / "value")
+ .set_coercer([this, chan_idx](const double gain){
+ return this->set_tx_gain(gain, chan_idx);
+ })
+ .set_publisher([this, chan_idx](){
+ return radio_ctrl_impl::get_tx_gain(chan_idx);
+ })
+ ;
+ subtree->create<meta_range_t>(tx_fe_path / "gains" / "all" / "range")
+ .add_coerced_subscriber([](const meta_range_t &){
+ throw uhd::runtime_error(
+ "Attempting to update gain range!");
+ })
+ .set_publisher([](){
+ return rhodium_radio_ctrl_impl::_get_gain_range(TX_DIRECTION);
+ })
+ ;
+
+ subtree->create<std::vector<std::string>>(tx_fe_path / "gains/all/profile/options")
+ .set(RHODIUM_GP_OPTIONS);
+
+ subtree->create<std::string>(tx_fe_path / "gains/all/profile/value")
+ .set_coercer([this](const std::string& profile){
+ std::string return_profile = profile;
+ if (!uhd::has(RHODIUM_GP_OPTIONS, profile))
+ {
+ return_profile = "default";
+ }
+ _gain_profile[TX_DIRECTION] = return_profile;
+ return return_profile;
+ })
+ .set_publisher([this](){
+ return _gain_profile[TX_DIRECTION];
+ })
+ ;
+
+ // RX gains
+ subtree->create<double>(rx_fe_path / "gains" / "all" / "value")
+ .set_coercer([this, chan_idx](const double gain){
+ return this->set_rx_gain(gain, chan_idx);
+ })
+ .set_publisher([this, chan_idx](){
+ return radio_ctrl_impl::get_rx_gain(chan_idx);
+ })
+ ;
+
+ subtree->create<meta_range_t>(rx_fe_path / "gains" / "all" / "range")
+ .add_coerced_subscriber([](const meta_range_t &){
+ throw uhd::runtime_error(
+ "Attempting to update gain range!");
+ })
+ .set_publisher([](){
+ return rhodium_radio_ctrl_impl::_get_gain_range(RX_DIRECTION);
+ })
+ ;
+
+ subtree->create<std::vector<std::string> >(rx_fe_path / "gains/all/profile/options")
+ .set(RHODIUM_GP_OPTIONS);
+
+ subtree->create<std::string>(rx_fe_path / "gains/all/profile/value")
+ .set_coercer([this](const std::string& profile){
+ std::string return_profile = profile;
+ if (!uhd::has(RHODIUM_GP_OPTIONS, profile))
+ {
+ return_profile = "default";
+ }
+ _gain_profile[RX_DIRECTION] = return_profile;
+ return return_profile;
+ })
+ .set_publisher([this](){
+ return _gain_profile[RX_DIRECTION];
+ })
+ ;
+
+ // TX LO lock sensor
+ subtree->create<sensor_value_t>(tx_fe_path / "sensors" / "lo_locked")
+ .set(sensor_value_t("all_los", false, "locked", "unlocked"))
+ .add_coerced_subscriber([](const sensor_value_t &){
+ throw uhd::runtime_error(
+ "Attempting to write to sensor!");
+ })
+ .set_publisher([this](){
+ return sensor_value_t(
+ "all_los",
+ this->get_lo_lock_status(TX_DIRECTION),
+ "locked", "unlocked"
+ );
+ })
+ ;
+ // RX LO lock sensor
+ subtree->create<sensor_value_t>(rx_fe_path / "sensors" / "lo_locked")
+ .set(sensor_value_t("all_los", false, "locked", "unlocked"))
+ .add_coerced_subscriber([](const sensor_value_t &){
+ throw uhd::runtime_error(
+ "Attempting to write to sensor!");
+ })
+ .set_publisher([this](){
+ return sensor_value_t(
+ "all_los",
+ this->get_lo_lock_status(RX_DIRECTION),
+ "locked", "unlocked"
+ );
+ })
+ ;
+ //LO Specific
+ //RX LO
+ //RX LO1 Frequency
+ subtree->create<double>(rx_fe_path / "los"/RHODIUM_LO1/"freq/value")
+ .set_publisher([this,chan_idx](){
+ return this->get_rx_lo_freq(RHODIUM_LO1, chan_idx);
+ })
+ .set_coercer([this,chan_idx](const double freq){
+ return this->set_rx_lo_freq(freq, RHODIUM_LO1, chan_idx);
+ })
+ ;
+ subtree->create<meta_range_t>(rx_fe_path / "los"/RHODIUM_LO1/"freq/range")
+ .set_publisher([this,chan_idx](){
+ return this->get_rx_lo_freq_range(RHODIUM_LO1, chan_idx);
+ })
+ ;
+ //RX LO1 Source
+ subtree->create<std::vector<std::string>>(rx_fe_path / "los"/RHODIUM_LO1/"source/options")
+ .set_publisher([this,chan_idx](){
+ return this->get_rx_lo_sources(RHODIUM_LO1, chan_idx);
+ })
+ ;
+ subtree->create<std::string>(rx_fe_path / "los"/RHODIUM_LO1/"source/value")
+ .add_coerced_subscriber([this,chan_idx](std::string src){
+ this->set_rx_lo_source(src, RHODIUM_LO1,chan_idx);
+ })
+ .set_publisher([this,chan_idx](){
+ return this->get_rx_lo_source(RHODIUM_LO1, chan_idx);
+ })
+ ;
+ //RX LO1 Export
+ subtree->create<bool>(rx_fe_path / "los"/RHODIUM_LO1/"export")
+ .add_coerced_subscriber([this,chan_idx](bool enabled){
+ this->set_rx_lo_export_enabled(enabled, RHODIUM_LO1, chan_idx);
+ })
+ ;
+ //RX LO1 Gain
+ subtree->create<double>(rx_fe_path / "los" /RHODIUM_LO1/ "gains" / RHODIUM_LO_GAIN / "value")
+ .set_publisher([this,chan_idx](){
+ return this->get_rx_lo_gain(RHODIUM_LO1, chan_idx);
+ })
+ .set_coercer([this,chan_idx](const double gain){
+ return this->set_rx_lo_gain(gain, RHODIUM_LO1, chan_idx);
+ })
+ ;
+ subtree->create<meta_range_t>(rx_fe_path / "los" /RHODIUM_LO1/ "gains" / RHODIUM_LO_GAIN / "range")
+ .set_publisher([](){
+ return rhodium_radio_ctrl_impl::_get_lo_gain_range();
+ })
+ .add_coerced_subscriber([](const meta_range_t &){
+ throw uhd::runtime_error("Attempting to update LO gain range!");
+ })
+ ;
+ //RX LO1 Output Power
+ subtree->create<double>(rx_fe_path / "los" /RHODIUM_LO1/ "gains" / RHODIUM_LO_POWER / "value")
+ .set_publisher([this,chan_idx](){
+ return this->get_rx_lo_power(RHODIUM_LO1, chan_idx);
+ })
+ .set_coercer([this,chan_idx](const double gain){
+ return this->set_rx_lo_power(gain, RHODIUM_LO1, chan_idx);
+ })
+ ;
+ subtree->create<meta_range_t>(rx_fe_path / "los" /RHODIUM_LO1/ "gains" / RHODIUM_LO_POWER / "range")
+ .set_publisher([](){
+ return rhodium_radio_ctrl_impl::_get_lo_power_range();
+ })
+ .add_coerced_subscriber([](const meta_range_t &){
+ throw uhd::runtime_error("Attempting to update LO output power range!");
+ })
+ ;
+ //RX LO2 Frequency
+ subtree->create<double>(rx_fe_path / "los"/RHODIUM_LO2/"freq/value")
+ .set_publisher([this,chan_idx](){
+ return this->get_rx_lo_freq(RHODIUM_LO2, chan_idx);
+ })
+ .set_coercer([this,chan_idx](double freq){
+ return this->set_rx_lo_freq(freq, RHODIUM_LO2, chan_idx);
+ })
+ ;
+ subtree->create<meta_range_t>(rx_fe_path / "los"/RHODIUM_LO2/"freq/range")
+ .set_publisher([this,chan_idx](){
+ return this->get_rx_lo_freq_range(RHODIUM_LO2, chan_idx);
+ })
+ ;
+ //RX LO2 Source
+ subtree->create<std::vector<std::string>>(rx_fe_path / "los"/RHODIUM_LO2/"source/options")
+ .set_publisher([this,chan_idx](){
+ return this->get_rx_lo_sources(RHODIUM_LO2, chan_idx);
+ })
+ ;
+ subtree->create<std::string>(rx_fe_path / "los"/RHODIUM_LO2/"source/value")
+ .add_coerced_subscriber([this,chan_idx](std::string src){
+ this->set_rx_lo_source(src, RHODIUM_LO2, chan_idx);
+ })
+ .set_publisher([this,chan_idx](){
+ return this->get_rx_lo_source(RHODIUM_LO2, chan_idx);
+ })
+ ;
+ //RX LO2 Export
+ subtree->create<bool>(rx_fe_path / "los"/RHODIUM_LO2/"export")
+ .add_coerced_subscriber([this,chan_idx](bool enabled){
+ this->set_rx_lo_export_enabled(enabled, RHODIUM_LO2, chan_idx);
+ });
+ //TX LO
+ //TX LO1 Frequency
+ subtree->create<double>(tx_fe_path / "los"/RHODIUM_LO1/"freq/value ")
+ .set_publisher([this,chan_idx](){
+ return this->get_tx_lo_freq(RHODIUM_LO1, chan_idx);
+ })
+ .set_coercer([this,chan_idx](double freq){
+ return this->set_tx_lo_freq(freq, RHODIUM_LO1, chan_idx);
+ })
+ ;
+ subtree->create<meta_range_t>(tx_fe_path / "los"/RHODIUM_LO1/"freq/range")
+ .set_publisher([this,chan_idx](){
+ return this->get_rx_lo_freq_range(RHODIUM_LO1, chan_idx);
+ })
+ ;
+ //TX LO1 Source
+ subtree->create<std::vector<std::string>>(tx_fe_path / "los"/RHODIUM_LO1/"source/options")
+ .set_publisher([this,chan_idx](){
+ return this->get_tx_lo_sources(RHODIUM_LO1, chan_idx);
+ })
+ ;
+ subtree->create<std::string>(tx_fe_path / "los"/RHODIUM_LO1/"source/value")
+ .add_coerced_subscriber([this,chan_idx](std::string src){
+ this->set_tx_lo_source(src, RHODIUM_LO1, chan_idx);
+ })
+ .set_publisher([this,chan_idx](){
+ return this->get_tx_lo_source(RHODIUM_LO1, chan_idx);
+ })
+ ;
+ //TX LO1 Export
+ subtree->create<bool>(tx_fe_path / "los"/RHODIUM_LO1/"export")
+ .add_coerced_subscriber([this,chan_idx](bool enabled){
+ this->set_tx_lo_export_enabled(enabled, RHODIUM_LO1, chan_idx);
+ })
+ ;
+ //TX LO1 Gain
+ subtree->create<double>(tx_fe_path / "los" /RHODIUM_LO1/ "gains" / RHODIUM_LO_GAIN / "value")
+ .set_publisher([this,chan_idx](){
+ return this->get_tx_lo_gain(RHODIUM_LO1, chan_idx);
+ })
+ .set_coercer([this,chan_idx](const double gain){
+ return this->set_tx_lo_gain(gain, RHODIUM_LO1, chan_idx);
+ })
+ ;
+ subtree->create<meta_range_t>(tx_fe_path / "los" /RHODIUM_LO1/ "gains" / RHODIUM_LO_GAIN / "range")
+ .set_publisher([](){
+ return rhodium_radio_ctrl_impl::_get_lo_gain_range();
+ })
+ .add_coerced_subscriber([](const meta_range_t &){
+ throw uhd::runtime_error("Attempting to update LO gain range!");
+ })
+ ;
+ //TX LO1 Output Power
+ subtree->create<double>(tx_fe_path / "los" /RHODIUM_LO1/ "gains" / RHODIUM_LO_POWER / "value")
+ .set_publisher([this,chan_idx](){
+ return this->get_tx_lo_power(RHODIUM_LO1, chan_idx);
+ })
+ .set_coercer([this,chan_idx](const double gain){
+ return this->set_tx_lo_power(gain, RHODIUM_LO1, chan_idx);
+ })
+ ;
+ subtree->create<meta_range_t>(tx_fe_path / "los" /RHODIUM_LO1/ "gains" / RHODIUM_LO_POWER / "range")
+ .set_publisher([](){
+ return rhodium_radio_ctrl_impl::_get_lo_power_range();
+ })
+ .add_coerced_subscriber([](const meta_range_t &){
+ throw uhd::runtime_error("Attempting to update LO output power range!");
+ })
+ ;
+ //TX LO2 Frequency
+ subtree->create<double>(tx_fe_path / "los"/RHODIUM_LO2/"freq/value")
+ .set_publisher([this,chan_idx](){
+ return this->get_tx_lo_freq(RHODIUM_LO2, chan_idx);
+ })
+ .set_coercer([this,chan_idx](double freq){
+ return this->set_tx_lo_freq(freq, RHODIUM_LO2, chan_idx);
+ })
+ ;
+ subtree->create<meta_range_t>(tx_fe_path / "los"/RHODIUM_LO2/"freq/range")
+ .set_publisher([this,chan_idx](){
+ return this->get_tx_lo_freq_range(RHODIUM_LO2,chan_idx);
+ })
+ ;
+ //TX LO2 Source
+ subtree->create<std::vector<std::string>>(tx_fe_path / "los"/RHODIUM_LO2/"source/options")
+ .set_publisher([this,chan_idx](){
+ return this->get_tx_lo_sources(RHODIUM_LO2, chan_idx);
+ })
+ ;
+ subtree->create<std::string>(tx_fe_path / "los"/RHODIUM_LO2/"source/value")
+ .add_coerced_subscriber([this,chan_idx](std::string src){
+ this->set_tx_lo_source(src, RHODIUM_LO2, chan_idx);
+ })
+ .set_publisher([this,chan_idx](){
+ return this->get_tx_lo_source(RHODIUM_LO2, chan_idx);
+ })
+ ;
+ //TX LO2 Export
+ subtree->create<bool>(tx_fe_path / "los"/RHODIUM_LO2/"export")
+ .add_coerced_subscriber([this,chan_idx](bool enabled){
+ this->set_tx_lo_export_enabled(enabled, RHODIUM_LO2, chan_idx);
+ })
+ ;
+}
+
+void rhodium_radio_ctrl_impl::_init_prop_tree()
+{
+ const fs_path fe_base = fs_path("dboards") / _radio_slot;
+ this->_init_frontend_subtree(_tree->subtree(fe_base), 0);
+
+ // EEPROM paths subject to change FIXME
+ _tree->create<eeprom_map_t>(_root_path / "eeprom")
+ .set(eeprom_map_t());
+
+ _tree->create<int>("rx_codecs" / _radio_slot / "gains");
+ _tree->create<int>("tx_codecs" / _radio_slot / "gains");
+ _tree->create<std::string>("rx_codecs" / _radio_slot / "name").set("ad9695-625");
+ _tree->create<std::string>("tx_codecs" / _radio_slot / "name").set("dac37j82");
+
+ // TODO remove this dirty hack
+ if (not _tree->exists("tick_rate"))
+ {
+ _tree->create<double>("tick_rate")
+ .set_publisher([this](){ return this->get_rate(); })
+ ;
+ }
+}
+
+void rhodium_radio_ctrl_impl::_init_mpm_sensors(
+ const direction_t dir,
+ const size_t chan_idx
+) {
+ const std::string trx = (dir == RX_DIRECTION) ? "RX" : "TX";
+ const fs_path fe_path =
+ fs_path("dboards") / _radio_slot /
+ (dir == RX_DIRECTION ? "rx_frontends" : "tx_frontends") / chan_idx;
+ auto sensor_list =
+ _rpcc->request_with_token<std::vector<std::string>>(
+ this->_rpc_prefix + "get_sensors", trx);
+ UHD_LOG_TRACE(unique_id(),
+ "Chan " << chan_idx << ": Found "
+ << sensor_list.size() << " " << trx << " sensors.");
+ for (const auto &sensor_name : sensor_list) {
+ UHD_LOG_TRACE(unique_id(),
+ "Adding " << trx << " sensor " << sensor_name);
+ _tree->create<sensor_value_t>(fe_path / "sensors" / sensor_name)
+ .add_coerced_subscriber([](const sensor_value_t &){
+ throw uhd::runtime_error(
+ "Attempting to write to sensor!");
+ })
+ .set_publisher([this, trx, sensor_name, chan_idx](){
+ return sensor_value_t(
+ this->_rpcc->request_with_token<sensor_value_t::sensor_map_t>(
+ this->_rpc_prefix + "get_sensor",
+ trx, sensor_name, chan_idx)
+ );
+ })
+ ;
+ }
+}
+
diff --git a/host/lib/usrp/dboard/rhodium/rhodium_radio_ctrl_lo.cpp b/host/lib/usrp/dboard/rhodium/rhodium_radio_ctrl_lo.cpp
new file mode 100644
index 000000000..49eb854d5
--- /dev/null
+++ b/host/lib/usrp/dboard/rhodium/rhodium_radio_ctrl_lo.cpp
@@ -0,0 +1,569 @@
+//
+// Copyright 2018 Ettus Research, a National Instruments Company
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+#include "rhodium_radio_ctrl_impl.hpp"
+#include "rhodium_constants.hpp"
+#include <uhdlib/utils/narrow.hpp>
+#include <uhd/utils/log.hpp>
+#include <uhd/utils/algorithm.hpp>
+#include <uhd/types/direction.hpp>
+#include <uhd/exception.hpp>
+#include <boost/format.hpp>
+
+using namespace uhd;
+using namespace uhd::usrp;
+using namespace uhd::rfnoc;
+
+namespace {
+ constexpr int NUM_THRESHOLDS = 13;
+ constexpr std::array<double, NUM_THRESHOLDS> FREQ_THRESHOLDS =
+ {0.45e9, 0.5e9, 1e9, 1.5e9, 2e9, 2.5e9, 3e9, 3.55e9, 4e9, 4.5e9, 5e9, 5.5e9, 6e9};
+ constexpr std::array<int, NUM_THRESHOLDS> LMX_GAIN_VALUES =
+ {18, 18, 17, 17, 17, 16, 12, 11, 11, 11, 10, 13, 18};
+ constexpr std::array<int, NUM_THRESHOLDS> DSA_GAIN_VALUES =
+ {19, 19, 21, 21, 20, 20, 22, 21, 20, 22, 22, 24, 26};
+
+ // Describes the lowband LO in terms of the master clock rate
+ const std::map<double, double> MCR_TO_LOWBAND_IF = {
+ {200e6, 1200e6},
+ {245.76e6, 1228.8e6},
+ {250e6, 1500e6},
+ };
+
+ // validation helpers
+
+ std::vector<std::string> _get_lo_names()
+ {
+ return { RHODIUM_LO1, RHODIUM_LO2 };
+ }
+
+ void _validate_lo_name(const std::string& name, const std::string& function_name)
+ {
+ if (!uhd::has(_get_lo_names(), name)) {
+ throw uhd::value_error(str(boost::format(
+ "%s was called with an invalid LO name: %s")
+ % function_name
+ % name));
+ }
+ }
+
+ // object agnostic helpers
+ std::vector<std::string> _get_lo_sources(const std::string& name)
+ {
+ if (name == RHODIUM_LO1) {
+ return { "internal", "external" };
+ } else {
+ return { "internal" };
+ }
+ }
+}
+
+/******************************************************************************
+ * Property Getters
+ *****************************************************************************/
+
+std::vector<std::string> rhodium_radio_ctrl_impl::get_tx_lo_names(
+ const size_t chan
+) {
+ UHD_LOG_TRACE(unique_id(), "get_tx_lo_names(chan=" << chan << ")");
+ UHD_ASSERT_THROW(chan == 0);
+
+ return _get_lo_names();
+}
+
+std::vector<std::string> rhodium_radio_ctrl_impl::get_rx_lo_names(
+ const size_t chan
+) {
+ UHD_LOG_TRACE(unique_id(), "get_rx_lo_names(chan=" << chan << ")");
+ UHD_ASSERT_THROW(chan == 0);
+
+ return _get_lo_names();
+}
+
+std::vector<std::string> rhodium_radio_ctrl_impl::get_tx_lo_sources(
+ const std::string& name,
+ const size_t chan
+) {
+ UHD_LOG_TRACE(unique_id(), "get_tx_lo_sources(name=" << name << ", chan=" << chan << ")");
+ UHD_ASSERT_THROW(chan == 0);
+ _validate_lo_name(name, "get_tx_lo_sources");
+
+ return _get_lo_sources(name);
+}
+
+std::vector<std::string> rhodium_radio_ctrl_impl::get_rx_lo_sources(
+ const std::string& name,
+ const size_t chan
+) {
+ UHD_LOG_TRACE(unique_id(), "get_rx_lo_sources(name=" << name << ", chan=" << chan << ")");
+ UHD_ASSERT_THROW(chan == 0);
+ _validate_lo_name(name, "get_rx_lo_sources");
+
+ return _get_lo_sources(name);
+}
+
+freq_range_t rhodium_radio_ctrl_impl::_get_lo_freq_range(const std::string &name) const
+{
+ if (name == RHODIUM_LO1) {
+ return freq_range_t{ RHODIUM_LO1_MIN_FREQ, RHODIUM_LO1_MAX_FREQ };
+ } else {
+ // The Lowband LO is a fixed frequency
+ return freq_range_t{ _get_lowband_lo_freq(), _get_lowband_lo_freq() };
+ }
+}
+
+freq_range_t rhodium_radio_ctrl_impl::get_tx_lo_freq_range(
+ const std::string& name,
+ const size_t chan
+) {
+ UHD_LOG_TRACE(unique_id(), "get_tx_lo_freq_range(name=" << name << ", chan=" << chan << ")");
+ UHD_ASSERT_THROW(chan == 0);
+ _validate_lo_name(name, "get_tx_lo_freq_range");
+
+ return _get_lo_freq_range(name);
+}
+
+freq_range_t rhodium_radio_ctrl_impl::get_rx_lo_freq_range(
+ const std::string& name,
+ const size_t chan
+) {
+ UHD_LOG_TRACE(unique_id(), "get_rx_lo_freq_range(name=" << name << ", chan=" << chan << ")");
+ UHD_ASSERT_THROW(chan == 0);
+ _validate_lo_name(name, "get_rx_lo_freq_range");
+
+ return _get_lo_freq_range(name);
+}
+
+/******************************************************************************
+ * Frequency Control
+ *****************************************************************************/
+
+double rhodium_radio_ctrl_impl::set_tx_lo_freq(
+ const double freq,
+ const std::string& name,
+ const size_t chan
+) {
+ UHD_LOG_TRACE(unique_id(), "set_tx_lo_freq(freq=" << freq << ", name=" << name << ", chan=" << chan << ")");
+ UHD_ASSERT_THROW(chan == 0);
+ _validate_lo_name(name, "set_tx_lo_freq");
+
+ if (name == RHODIUM_LO2) {
+ UHD_LOG_WARNING(unique_id(), "The Lowband LO cannot be tuned");
+ return _get_lowband_lo_freq();
+ }
+
+ const auto sd_enabled = _get_spur_dodging_enabled(TX_DIRECTION);
+ const auto sd_threshold = _get_spur_dodging_threshold(TX_DIRECTION);
+
+ _tx_lo_freq = _tx_lo->set_frequency(freq, sd_enabled, sd_threshold);
+ set_tx_lo_gain(_get_lo_dsa_setting(_tx_lo_freq), RHODIUM_LO1, chan);
+ set_tx_lo_power(_get_lo_power_setting(_tx_lo_freq), RHODIUM_LO1, chan);
+ _cpld->set_tx_lo_path(_tx_lo_freq);
+
+ return _tx_lo_freq;
+}
+
+double rhodium_radio_ctrl_impl::set_rx_lo_freq(
+ const double freq,
+ const std::string& name,
+ const size_t chan
+) {
+ UHD_LOG_TRACE(unique_id(), "set_rx_lo_freq(freq=" << freq << ", name=" << name << ", chan=" << chan << ")");
+ UHD_ASSERT_THROW(chan == 0);
+ _validate_lo_name(name, "set_rx_lo_freq");
+
+ if (name == RHODIUM_LO2) {
+ UHD_LOG_WARNING(unique_id(), "The Lowband LO cannot be tuned");
+ return _get_lowband_lo_freq();
+ }
+
+ const auto sd_enabled = _get_spur_dodging_enabled(RX_DIRECTION);
+ const auto sd_threshold = _get_spur_dodging_threshold(RX_DIRECTION);
+
+ _rx_lo_freq = _rx_lo->set_frequency(freq, sd_enabled, sd_threshold);
+ set_rx_lo_gain(_get_lo_dsa_setting(_rx_lo_freq), RHODIUM_LO1, chan);
+ set_rx_lo_power(_get_lo_power_setting(_rx_lo_freq), RHODIUM_LO1, chan);
+ _cpld->set_rx_lo_path(_rx_lo_freq);
+
+ return _rx_lo_freq;
+}
+
+double rhodium_radio_ctrl_impl::get_tx_lo_freq(
+ const std::string& name,
+ const size_t chan
+) {
+ UHD_LOG_TRACE(unique_id(), "get_tx_lo_freq(name=" << name << ", chan=" << chan << ")");
+ UHD_ASSERT_THROW(chan == 0);
+ _validate_lo_name(name, "get_tx_lo_freq");
+
+ return (name == RHODIUM_LO1) ? _tx_lo_freq : _get_lowband_lo_freq();
+}
+
+double rhodium_radio_ctrl_impl::get_rx_lo_freq(
+ const std::string& name,
+ const size_t chan
+) {
+ UHD_LOG_TRACE(unique_id(), "get_rx_lo_freq(name=" << name << ", chan=" << chan << ")");
+ UHD_ASSERT_THROW(chan == 0);
+ _validate_lo_name(name, "get_rx_lo_freq");
+
+ return (name == RHODIUM_LO1) ? _rx_lo_freq : _get_lowband_lo_freq();
+}
+
+/******************************************************************************
+ * Source Control
+ *****************************************************************************/
+
+void rhodium_radio_ctrl_impl::set_tx_lo_source(
+ const std::string& src,
+ const std::string& name,
+ const size_t chan
+) {
+ UHD_LOG_TRACE(unique_id(), "set_tx_lo_source(src=" << src << ", name=" << name << ", chan=" << chan << ")");
+ UHD_ASSERT_THROW(chan == 0);
+ _validate_lo_name(name, "set_tx_lo_source");
+
+ if (name == RHODIUM_LO2) {
+ if (src != "internal") {
+ UHD_LOG_ERROR(unique_id(), "The Lowband LO can only be set to internal");
+ }
+ return;
+ }
+
+ if (src == "internal") {
+ _tx_lo->set_output_enable(lmx2592_iface::output_t::RF_OUTPUT_A, true);
+ _cpld->set_tx_lo_source(rhodium_cpld_ctrl::tx_lo_input_sel_t::TX_LO_INPUT_SEL_INTERNAL);
+ } else if (src == "external") {
+ _tx_lo->set_output_enable(lmx2592_iface::output_t::RF_OUTPUT_A, false);
+ _cpld->set_tx_lo_source(rhodium_cpld_ctrl::tx_lo_input_sel_t::TX_LO_INPUT_SEL_EXTERNAL);
+ } else {
+ throw uhd::value_error(str(boost::format("set_tx_lo_source was called with an invalid LO source: %s Valid sources are [internal, external]") % src));
+ }
+
+ _tx_lo_source = src;
+}
+
+void rhodium_radio_ctrl_impl::set_rx_lo_source(
+ const std::string& src,
+ const std::string& name,
+ const size_t chan
+) {
+ UHD_LOG_TRACE(unique_id(), "set_rx_lo_source(src=" << src << ", name=" << name << ", chan=" << chan << ")");
+ UHD_ASSERT_THROW(chan == 0);
+ _validate_lo_name(name, "set_tx_lo_source");
+
+ if (name == RHODIUM_LO2) {
+ if (src != "internal") {
+ UHD_LOG_WARNING(unique_id(), "The Lowband LO can only be internal");
+ }
+ return;
+ }
+
+ if (src == "internal") {
+ _rx_lo->set_output_enable(lmx2592_iface::output_t::RF_OUTPUT_A, true);
+ _cpld->set_rx_lo_source(rhodium_cpld_ctrl::rx_lo_input_sel_t::RX_LO_INPUT_SEL_INTERNAL);
+ } else if (src == "external") {
+ _rx_lo->set_output_enable(lmx2592_iface::output_t::RF_OUTPUT_A, false);
+ _cpld->set_rx_lo_source(rhodium_cpld_ctrl::rx_lo_input_sel_t::RX_LO_INPUT_SEL_EXTERNAL);
+ } else {
+ throw uhd::value_error(str(boost::format("set_rx_lo_source was called with an invalid LO source: %s Valid sources for LO1 are [internal, external]") % src));
+ }
+
+ _rx_lo_source = src;
+}
+
+const std::string rhodium_radio_ctrl_impl::get_tx_lo_source(
+ const std::string& name,
+ const size_t chan
+) {
+ UHD_LOG_TRACE(unique_id(), "get_tx_lo_source(name=" << name << ", chan=" << chan << ")");
+ UHD_ASSERT_THROW(chan == 0);
+ _validate_lo_name(name, "get_tx_lo_source");
+
+ return (name == RHODIUM_LO1) ? _tx_lo_source : "internal";
+}
+
+const std::string rhodium_radio_ctrl_impl::get_rx_lo_source(
+ const std::string& name,
+ const size_t chan
+) {
+ UHD_LOG_TRACE(unique_id(), "get_rx_lo_source(name=" << name << ", chan=" << chan << ")");
+ UHD_ASSERT_THROW(chan == 0);
+ _validate_lo_name(name, "get_rx_lo_source");
+
+ return (name == RHODIUM_LO1) ? _rx_lo_source : "internal";
+}
+
+/******************************************************************************
+ * Export Control
+ *****************************************************************************/
+
+void rhodium_radio_ctrl_impl::_set_lo1_export_enabled(
+ const bool enabled,
+ const direction_t dir
+) {
+ auto& _lo_ctrl = (dir == RX_DIRECTION) ? _rx_lo : _tx_lo;
+ _lo_ctrl->set_output_enable(lmx2592_iface::output_t::RF_OUTPUT_B, enabled);
+}
+
+void rhodium_radio_ctrl_impl::set_tx_lo_export_enabled(
+ const bool enabled,
+ const std::string& name,
+ const size_t chan
+) {
+ UHD_LOG_TRACE(unique_id(), "set_tx_lo_export_enabled(enabled=" << enabled << ", name=" << name << ", chan=" << chan << ")");
+ UHD_ASSERT_THROW(chan == 0);
+ _validate_lo_name(name, "set_tx_lo_export_enabled");
+
+ if (name == RHODIUM_LO2) {
+ if (enabled) {
+ throw uhd::value_error("The lowband LO cannot be exported");
+ }
+ return;
+ }
+
+ _set_lo1_export_enabled(enabled, TX_DIRECTION);
+ _tx_lo_exported = enabled;
+}
+
+void rhodium_radio_ctrl_impl::set_rx_lo_export_enabled(
+ const bool enabled,
+ const std::string& name,
+ const size_t chan
+) {
+ UHD_LOG_TRACE(unique_id(), "set_rx_lo_export_enabled(enabled=" << enabled << ", name=" << name << ", chan=" << chan << ")");
+ UHD_ASSERT_THROW(chan == 0);
+ _validate_lo_name(name, "set_rx_lo_export_enabled");
+
+ if (name == RHODIUM_LO2) {
+ if (enabled) {
+ throw uhd::value_error("The lowband LO cannot be exported");
+ }
+ return;
+ }
+
+ _set_lo1_export_enabled(enabled, RX_DIRECTION);
+ _rx_lo_exported = enabled;
+}
+
+bool rhodium_radio_ctrl_impl::get_tx_lo_export_enabled(
+ const std::string& name,
+ const size_t chan
+) {
+ UHD_LOG_TRACE(unique_id(), "get_tx_lo_export_enabled(name=" << name << ", chan=" << chan << ")");
+ UHD_ASSERT_THROW(chan == 0);
+ _validate_lo_name(name, "get_tx_lo_export_enabled");
+
+ return (name == RHODIUM_LO1) ? _tx_lo_exported : false;
+}
+
+bool rhodium_radio_ctrl_impl::get_rx_lo_export_enabled(
+ const std::string& name,
+ const size_t chan
+) {
+ UHD_LOG_TRACE(unique_id(), "get_rx_lo_export_enabled(name=" << name << ", chan=" << chan << ")");
+ UHD_ASSERT_THROW(chan == 0);
+ _validate_lo_name(name, "get_rx_lo_export_enabled");
+
+ return (name == RHODIUM_LO1) ? _rx_lo_exported : false;
+}
+
+/******************************************************************************
+ * Gain Control
+ *****************************************************************************/
+
+double rhodium_radio_ctrl_impl::set_tx_lo_gain(
+ double gain,
+ const std::string& name,
+ const size_t chan
+) {
+ UHD_LOG_TRACE(unique_id(), "set_tx_lo_gain(gain=" << gain << ", name=" << name << ", chan=" << chan << ")");
+ UHD_ASSERT_THROW(chan == 0);
+ _validate_lo_name(name, "set_tx_lo_gain");
+
+ if (name == RHODIUM_LO2) {
+ UHD_LOG_WARNING(unique_id(), "The Lowband LO does not have configurable gain");
+ return 0.0;
+ }
+
+ auto index = _get_lo_gain_range().clip(gain);
+
+ _cpld->set_lo_gain(index, TX_DIRECTION);
+ _lo_tx_gain = index;
+ return _lo_tx_gain;
+}
+
+double rhodium_radio_ctrl_impl::set_rx_lo_gain(
+ double gain,
+ const std::string& name,
+ const size_t chan
+) {
+ UHD_LOG_TRACE(unique_id(), "set_rx_lo_gain(gain=" << gain << ", name=" << name << ", chan=" << chan << ")");
+ UHD_ASSERT_THROW(chan == 0);
+ _validate_lo_name(name, "set_rx_lo_gain");
+
+ if (name == RHODIUM_LO2) {
+ UHD_LOG_WARNING(unique_id(), "The Lowband LO does not have configurable gain");
+ return 0.0;
+ }
+
+ auto index = _get_lo_gain_range().clip(gain);
+
+ _cpld->set_lo_gain(index, RX_DIRECTION);
+ _lo_rx_gain = index;
+ return _lo_rx_gain;
+}
+
+double rhodium_radio_ctrl_impl::get_tx_lo_gain(
+ const std::string& name,
+ const size_t chan
+) {
+ UHD_LOG_TRACE(unique_id(), "get_tx_lo_gain(name=" << name << ", chan=" << chan << ")");
+ UHD_ASSERT_THROW(chan == 0);
+ _validate_lo_name(name, "get_tx_lo_gain");
+
+ return (name == RHODIUM_LO1) ? _lo_rx_gain : 0.0;
+}
+
+double rhodium_radio_ctrl_impl::get_rx_lo_gain(
+ const std::string& name,
+ const size_t chan
+) {
+ UHD_LOG_TRACE(unique_id(), "get_rx_lo_gain(name=" << name << ", chan=" << chan << ")");
+ UHD_ASSERT_THROW(chan == 0);
+ _validate_lo_name(name, "get_rx_lo_gain");
+
+ return (name == RHODIUM_LO1) ? _lo_tx_gain : 0.0;
+}
+
+/******************************************************************************
+ * Output Power Control
+ *****************************************************************************/
+
+double rhodium_radio_ctrl_impl::_set_lo1_power(
+ const double power,
+ const direction_t dir
+) {
+ auto& _lo_ctrl = (dir == RX_DIRECTION) ? _rx_lo : _tx_lo;
+ auto coerced_power = static_cast<uint32_t>(_get_lo_power_range().clip(power, true));
+
+ _lo_ctrl->set_output_power(lmx2592_iface::RF_OUTPUT_A, coerced_power);
+ return coerced_power;
+}
+
+double rhodium_radio_ctrl_impl::set_tx_lo_power(
+ const double power,
+ const std::string& name,
+ const size_t chan
+) {
+ UHD_LOG_TRACE(unique_id(), "set_tx_lo_power(power=" << power << ", name=" << name << ", chan=" << chan << ")");
+ UHD_ASSERT_THROW(chan == 0);
+ _validate_lo_name(name, "set_tx_lo_power");
+
+ if (name == RHODIUM_LO2) {
+ UHD_LOG_WARNING(unique_id(), "The Lowband LO does not have configurable output power");
+ return 0.0;
+ }
+
+ _lo_tx_power = _set_lo1_power(power, TX_DIRECTION);
+ return _lo_tx_power;
+}
+
+double rhodium_radio_ctrl_impl::set_rx_lo_power(
+ const double power,
+ const std::string& name,
+ const size_t chan
+) {
+ UHD_LOG_TRACE(unique_id(), "set_rx_lo_power(power=" << power << ", name=" << name << ", chan=" << chan << ")");
+ UHD_ASSERT_THROW(chan == 0);
+ _validate_lo_name(name, "set_rx_lo_power");
+
+ if (name == RHODIUM_LO2) {
+ UHD_LOG_WARNING(unique_id(), "The Lowband LO does not have configurable output power");
+ return 0.0;
+ }
+
+ _lo_rx_power = _set_lo1_power(power, RX_DIRECTION);
+ return _lo_rx_power;
+}
+
+double rhodium_radio_ctrl_impl::get_tx_lo_power(
+ const std::string& name,
+ const size_t chan
+) {
+ UHD_LOG_TRACE(unique_id(), "get_tx_lo_power(name=" << name << ", chan=" << chan << ")");
+ UHD_ASSERT_THROW(chan == 0);
+ _validate_lo_name(name, "get_tx_lo_power");
+
+ return (name == RHODIUM_LO1) ? _lo_tx_power : 0.0;
+}
+
+double rhodium_radio_ctrl_impl::get_rx_lo_power(
+ const std::string& name,
+ const size_t chan
+) {
+ UHD_LOG_TRACE(unique_id(), "get_rx_lo_power(name=" << name << ", chan=" << chan << ")");
+ UHD_ASSERT_THROW(chan == 0);
+ _validate_lo_name(name, "get_rx_lo_power");
+
+ return (name == RHODIUM_LO1) ? _lo_rx_power : 0.0;
+}
+
+/******************************************************************************
+ * Helper Functions
+ *****************************************************************************/
+
+double rhodium_radio_ctrl_impl::_get_lowband_lo_freq() const
+{
+ return MCR_TO_LOWBAND_IF.at(_master_clock_rate);
+}
+
+uhd::gain_range_t rhodium_radio_ctrl_impl::_get_lo_gain_range()
+{
+ return gain_range_t(LO_MIN_GAIN, LO_MAX_GAIN, LO_GAIN_STEP);
+}
+
+uhd::gain_range_t rhodium_radio_ctrl_impl::_get_lo_power_range()
+{
+ return gain_range_t(LO_MIN_POWER, LO_MAX_POWER, LO_POWER_STEP);
+}
+
+int rhodium_radio_ctrl_impl::_get_lo_dsa_setting(double freq) {
+ int index = 0;
+ while (freq > FREQ_THRESHOLDS[index+1]) {
+ index++;
+ }
+
+ const double freq_low = FREQ_THRESHOLDS[index];
+ const double freq_high = FREQ_THRESHOLDS[index+1];
+ const double gain_low = DSA_GAIN_VALUES[index];
+ const double gain_high = DSA_GAIN_VALUES[index+1];
+
+ const double slope = (gain_high - gain_low) / (freq_high - freq_low);
+ const double gain_at_freq = gain_low + (freq - freq_low) * slope;
+
+ UHD_LOG_TRACE(unique_id(), "Interpolated DSA Gain is " << gain_at_freq);
+ return static_cast<int>(std::round(gain_at_freq));
+}
+
+unsigned int rhodium_radio_ctrl_impl::_get_lo_power_setting(double freq) {
+ int index = 0;
+ while (freq > FREQ_THRESHOLDS[index+1]) {
+ index++;
+ }
+
+ const double freq_low = FREQ_THRESHOLDS[index];
+ const double freq_high = FREQ_THRESHOLDS[index+1];
+ const double power_low = LMX_GAIN_VALUES[index];
+ const double power_high = LMX_GAIN_VALUES[index+1];
+
+
+ const double slope = (power_high - power_low) / (freq_high - freq_low);
+ const double power_at_freq = power_low + (freq - freq_low) * slope;
+
+ UHD_LOG_TRACE(unique_id(), "Interpolated LMX Power is " << power_at_freq);
+ return uhd::narrow_cast<unsigned int>(std::lround(power_at_freq));
+}
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',
}
#########################################################################