diff options
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', } ######################################################################### |