diff options
| author | Mark Meserve <mark.meserve@ni.com> | 2018-10-24 15:24:21 -0500 | 
|---|---|---|
| committer | Brent Stapleton <bstapleton@g.hmc.edu> | 2018-10-25 10:30:59 -0700 | 
| commit | d87ae61d0490339bfbd7de5d57e692e9e8961237 (patch) | |
| tree | 3e11b3c5f3c0aab3624dadf78ddcbcb2af85b3b9 | |
| parent | ec2977d8cbb233988b93f81deab7af99daec4165 (diff) | |
| download | uhd-d87ae61d0490339bfbd7de5d57e692e9e8961237.tar.gz uhd-d87ae61d0490339bfbd7de5d57e692e9e8961237.tar.bz2 uhd-d87ae61d0490339bfbd7de5d57e692e9e8961237.zip  | |
rh: add support for rhodium devices
Co-authored-by: Humberto Jimenez <humberto.jimenez@ni.com>
Co-authored-by: Alex Williams <alex.williams@ni.com>
Co-authored-by: Derek Kozel <derek.kozel@ni.com>
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',      }      #########################################################################  | 
