aboutsummaryrefslogtreecommitdiffstats
path: root/host/lib/usrp
diff options
context:
space:
mode:
Diffstat (limited to 'host/lib/usrp')
-rw-r--r--host/lib/usrp/CMakeLists.txt1
-rw-r--r--host/lib/usrp/common/CMakeLists.txt1
-rw-r--r--host/lib/usrp/common/lmx2572.cpp1030
-rw-r--r--host/lib/usrp/dboard/CMakeLists.txt7
-rw-r--r--host/lib/usrp/dboard/zbx/CMakeLists.txt17
-rw-r--r--host/lib/usrp/dboard/zbx/zbx_cpld_ctrl.cpp931
-rw-r--r--host/lib/usrp/dboard/zbx/zbx_dboard.cpp758
-rw-r--r--host/lib/usrp/dboard/zbx/zbx_dboard_init.cpp685
-rw-r--r--host/lib/usrp/dboard/zbx/zbx_expert.cpp672
-rw-r--r--host/lib/usrp/dboard/zbx/zbx_lo_ctrl.cpp162
-rw-r--r--host/lib/usrp/mpmd/mpmd_devices.hpp2
-rw-r--r--host/lib/usrp/mpmd/mpmd_image_loader.cpp17
-rw-r--r--host/lib/usrp/mpmd/mpmd_mb_controller.cpp64
-rw-r--r--host/lib/usrp/x300/x300_mb_controller.hpp4
-rw-r--r--host/lib/usrp/x400/CMakeLists.txt20
-rw-r--r--host/lib/usrp/x400/adc_self_calibration.cpp196
-rw-r--r--host/lib/usrp/x400/adc_self_calibration.hpp46
-rw-r--r--host/lib/usrp/x400/x400_radio_control.cpp752
-rw-r--r--host/lib/usrp/x400/x400_radio_control.hpp198
-rw-r--r--host/lib/usrp/x400/x400_rfdc_control.cpp73
20 files changed, 5628 insertions, 8 deletions
diff --git a/host/lib/usrp/CMakeLists.txt b/host/lib/usrp/CMakeLists.txt
index f15324608..570972f16 100644
--- a/host/lib/usrp/CMakeLists.txt
+++ b/host/lib/usrp/CMakeLists.txt
@@ -41,3 +41,4 @@ INCLUDE_SUBDIRECTORY(usrp2)
INCLUDE_SUBDIRECTORY(b100)
INCLUDE_SUBDIRECTORY(x300)
INCLUDE_SUBDIRECTORY(b200)
+INCLUDE_SUBDIRECTORY(x400)
diff --git a/host/lib/usrp/common/CMakeLists.txt b/host/lib/usrp/common/CMakeLists.txt
index a61738743..e37a38110 100644
--- a/host/lib/usrp/common/CMakeLists.txt
+++ b/host/lib/usrp/common/CMakeLists.txt
@@ -30,6 +30,7 @@ LIBUHD_APPEND_SOURCES(
${CMAKE_CURRENT_SOURCE_DIR}/adf435x.cpp
${CMAKE_CURRENT_SOURCE_DIR}/adf535x.cpp
${CMAKE_CURRENT_SOURCE_DIR}/lmx2592.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/lmx2572.cpp
${CMAKE_CURRENT_SOURCE_DIR}/apply_corrections.cpp
${CMAKE_CURRENT_SOURCE_DIR}/validate_subdev_spec.cpp
${CMAKE_CURRENT_SOURCE_DIR}/recv_packet_demuxer.cpp
diff --git a/host/lib/usrp/common/lmx2572.cpp b/host/lib/usrp/common/lmx2572.cpp
new file mode 100644
index 000000000..c56478b1d
--- /dev/null
+++ b/host/lib/usrp/common/lmx2572.cpp
@@ -0,0 +1,1030 @@
+//
+// Copyright 2020 Ettus Research, A National Instruments Brand
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+
+#include "lmx2572_regs.hpp"
+#include <uhd/exception.hpp>
+#include <uhd/utils/log.hpp>
+#include <uhd/utils/math.hpp>
+#include <uhdlib/usrp/common/lmx2572.hpp>
+#include <uhdlib/utils/interpolation.hpp>
+#include <uhdlib/utils/narrow.hpp>
+#include <cmath>
+#include <limits>
+#include <map>
+
+namespace {
+// LOG ID
+constexpr char LOG_ID[] = "LMX2572";
+// Highest LO / output frequency
+constexpr double MAX_OUT_FREQ = 6.4e9; // Hz
+// Lowest LO / output frequency
+constexpr double MIN_OUT_FREQ = 12.5e6; // Hz
+// Target loop bandwidth
+constexpr double TARGET_LOOP_BANDWIDTH = 75e3; // Hz
+// Loop Filter gain setting resistor
+constexpr double LOOP_GAIN_SETTING_RESISTANCE = 150; // ohm
+// Delay after powerup. TI recommends a 10 ms delay after clearing powerdown
+// (not documented in the datasheet).
+const uhd::time_spec_t POWERUP_DELAY = uhd::time_spec_t(10e-3);
+// Conservative estimate for PLL to lock which includes VCO calibration and PLL settling
+constexpr double PLL_LOCK_TIME = 200e-6; // s
+
+// Valid input/reference frequencies (fOSC)
+//
+// NOTE: These frequencies are valid for X400/ZBX. If we need to use this
+// driver elsewhere, this part needs to be refactored.
+const std::set<double> VALID_FOSC{61.44e6, 64e6, 62.5e6, 50e6};
+
+
+}; // namespace
+
+//! Control interface for an LMX2572 synthesizer
+class lmx2572_impl : public lmx2572_iface
+{
+public:
+ enum class muxout_state_t { LOCKDETECT, SDO };
+
+ explicit lmx2572_impl(
+ write_fn_t&& poke_fn, read_fn_t&& peek_fn, sleep_fn_t&& sleep_fn)
+ : _poke16(std::move(poke_fn))
+ , _peek16(std::move(peek_fn))
+ , _sleep(std::move(sleep_fn))
+ , _regs()
+ {
+ _regs.save_state();
+ }
+
+ void commit() override
+ {
+ UHD_LOG_TRACE(LOG_ID, "Storing register cache to LMX2572...");
+ const auto changed_addrs = _regs.get_changed_addrs<uint8_t>();
+ for (const auto addr : changed_addrs) {
+ // We write R0 last, for double-buffering
+ if (addr == 0) {
+ continue;
+ }
+ _poke16(addr, _regs.get_reg(addr));
+ }
+ _poke16(0, _regs.get_reg(0));
+ _regs.save_state();
+ UHD_LOG_TRACE(LOG_ID,
+ "Storing cache complete: Updated " << changed_addrs.size() << " registers.");
+ }
+
+ bool get_enabled() override
+ {
+ // Chip is either in normal operation mode or power down mode
+ return _regs.powerdown == lmx2572_regs_t::powerdown_t::POWERDOWN_NORMAL_OPERATION;
+ }
+
+ void set_enabled(const bool enabled) override
+ {
+ const bool prev_enabled = get_enabled();
+
+ _regs.powerdown = enabled
+ ? lmx2572_regs_t::powerdown_t::POWERDOWN_NORMAL_OPERATION
+ : lmx2572_regs_t::powerdown_t::POWERDOWN_POWER_DOWN;
+ _poke16(0, _regs.get_reg(0));
+
+ if (enabled && !prev_enabled) {
+ _sleep(POWERUP_DELAY);
+ }
+ }
+
+ void reset() override
+ {
+ // Power-on Programming Sequence described in the datasheet,
+ // Section 7.5.1.1
+ _regs = lmx2572_regs_t{};
+ _regs.reset = lmx2572_regs_t::reset_t::RESET_RESET;
+ _poke16(0, _regs.get_reg(0));
+ // Reset bit is self-clearing, it does not need to be poked twice. We
+ // manually reset it in the SW cache so we don't accidentally reset again
+ _regs.reset = lmx2572_regs_t::reset_t::RESET_NORMAL_OPERATION;
+ // Also enable register readback so we can read back the magic number
+ // register
+ _enable_register_readback(true);
+ // If the LO was previously powered down, the reset above will power it up. On
+ // power-up, we need to wait for the power up delay recommended by TI.
+ _sleep(POWERUP_DELAY);
+ // Check we can read back the last register, which always returns a
+ // magic constant:
+ const auto magic125 = _peek16(125);
+ if (magic125 != 0x2288) {
+ UHD_LOG_ERROR(LOG_ID,
+ "Unable to communicate with LMX2572! Expected R125==0x2288, got: "
+ << std::hex << magic125 << std::dec);
+ throw uhd::runtime_error("Unable to communicate to LMX2572!");
+ }
+ UHD_LOG_TRACE(LOG_ID, "Communication with LMX2572 successful.");
+ // Now set _regs into a sensible state
+ _set_defaults();
+ // Now write the regs in reverse order, skipping RO regs
+ const auto ro_regs = _regs.get_ro_regs();
+ // Write R0 last for the double buffering
+ for (int addr = _regs.get_num_regs() - 2; addr >= 0; addr--) {
+ if (ro_regs.count(addr)) {
+ continue;
+ }
+ _poke16(uhd::narrow_cast<uint8_t>(addr), _regs.get_reg(addr));
+ }
+ _regs.save_state();
+ }
+
+ bool get_lock_status() override
+ {
+ // Disable register readback which implicitly enables lock detect mode
+ _enable_register_readback(false);
+ // If the PLL is locked we expect to read 0xFFFF from any read
+ return _peek16(0) == 0xFFFF;
+ }
+
+ uint16_t peek16(const uint8_t addr)
+ {
+ _enable_register_readback(true);
+ return _peek16(addr);
+ }
+
+ void set_sync_mode(const bool enable) override
+ {
+ _sync_mode = enable;
+ }
+
+ //! Returns the enabled/disabled state of the phase synchronization
+ bool get_sync_mode() override
+ {
+ return _sync_mode;
+ }
+
+ void set_output_enable_all(const bool enable) override
+ {
+ set_output_enable(RF_OUTPUT_A, enable);
+ set_output_enable(RF_OUTPUT_B, enable);
+ }
+
+ void set_output_enable(const output_t output, const bool enable) override
+ {
+ if (output == RF_OUTPUT_A) {
+ _regs.outa_pd = enable ? lmx2572_regs_t::outa_pd_t::OUTA_PD_NORMAL_OPERATION
+ : lmx2572_regs_t::outa_pd_t::OUTA_PD_POWER_DOWN;
+ return;
+ }
+ if (output == RF_OUTPUT_B) {
+ _regs.outb_pd = enable ? lmx2572_regs_t::outb_pd_t::OUTB_PD_NORMAL_OPERATION
+ : lmx2572_regs_t::outb_pd_t::OUTB_PD_POWER_DOWN;
+ return;
+ }
+ UHD_THROW_INVALID_CODE_PATH();
+ }
+
+ void set_output_power(const output_t output, const uint8_t power) override
+ {
+ if (output == RF_OUTPUT_A) {
+ _regs.outa_pwr = power;
+ return;
+ }
+ if (output == RF_OUTPUT_B) {
+ _regs.outb_pwr = power;
+ return;
+ }
+ UHD_THROW_INVALID_CODE_PATH();
+ }
+
+ void set_mux_input(const output_t output, const mux_in_t input) override
+ {
+ switch (output) {
+ case RF_OUTPUT_A: {
+ switch (input) {
+ case mux_in_t::DIVIDER:
+ _regs.outa_mux =
+ lmx2572_regs_t::outa_mux_t::OUTA_MUX_CHANNEL_DIVIDER;
+ return;
+ case mux_in_t::VCO:
+ _regs.outa_mux = lmx2572_regs_t::outa_mux_t::OUTA_MUX_VCO;
+ return;
+ case mux_in_t::HIGH_IMPEDANCE:
+ _regs.outa_mux =
+ lmx2572_regs_t::outa_mux_t::OUTA_MUX_HIGH_IMPEDANCE;
+ return;
+ default:
+ break;
+ }
+ break;
+ }
+ case RF_OUTPUT_B: {
+ switch (input) {
+ case mux_in_t::DIVIDER:
+ _regs.outb_mux =
+ lmx2572_regs_t::outb_mux_t::OUTB_MUX_CHANNEL_DIVIDER;
+ return;
+ case mux_in_t::VCO:
+ _regs.outb_mux = lmx2572_regs_t::outb_mux_t::OUTB_MUX_VCO;
+ return;
+ case mux_in_t::HIGH_IMPEDANCE:
+ _regs.outb_mux =
+ lmx2572_regs_t::outb_mux_t::OUTB_MUX_HIGH_IMPEDANCE;
+ return;
+ case mux_in_t::SYSREF:
+ _regs.outb_mux = lmx2572_regs_t::outb_mux_t::OUTB_MUX_SYSREF;
+ return;
+ default:
+ break;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ UHD_THROW_INVALID_CODE_PATH();
+ }
+
+ double set_frequency(const double target_freq,
+ const double fOSC,
+ const bool spur_dodging) override
+ {
+ // Sanity check
+ if (target_freq > MAX_OUT_FREQ || target_freq < MIN_OUT_FREQ) {
+ UHD_LOG_ERROR(LOG_ID,
+ "Invalid LMX2572 target frequency! Must be in ["
+ << (MIN_OUT_FREQ / 1e6) << " MHz, " << (MAX_OUT_FREQ / 1e6)
+ << " MHz]!");
+ throw uhd::value_error("Invalid LMX2572 target frequency!");
+ }
+ UHD_ASSERT_THROW(VALID_FOSC.count(fOSC));
+ // Create an integer version of fOSC for some of the following
+ // calculations
+ const uint64_t fOSC_int = static_cast<uint64_t>(fOSC);
+
+ // 1. Set up output/channel divider value and the output mux
+ const uint16_t out_D = _set_output_divider(target_freq);
+ const double fVCO = target_freq * out_D;
+ UHD_ASSERT_THROW(3200e6 <= fVCO && fVCO <= 6400e6);
+
+ // 2. Configure the reference dividers/multipliers
+ _set_pll_div_and_mult(target_freq, fVCO, fOSC_int);
+
+ // Calculate phase detector frequency
+ // See datasheet (Section 7.3.2):
+ // Equation (1): fPD = fOSC × OSC_2X × MULT / (PLL_R_PRE × PLL_R)
+ const double fPD =
+ fOSC * (_regs.osc_2x + 1) * _regs.mult / (_regs.pll_r_pre * _regs.pll_r);
+
+ // pre-3. Identify SYNC category.
+ // Based on the category, we need to set VCO_PHASE_SYNC_EN appropriately
+ // and update our p-multiplier.
+ // Note: In the line below, we use target_freq and not actual_freq. That
+ // is OK, because we know that _get_sync_cat() only does a check to see
+ // if target_freq is an integer multiple of fOSC. If that's the case,
+ // then rounding/coercion errors won't happen between target_freq and
+ // actual_freq because we can always exactly produce frequencies that
+ // are integer multiples of fOSC. This way, we don't have a circular
+ // dependency (because actual_freq depends on p indirectly).
+ const int p =
+ _set_phase_sync(_get_sync_cat(_regs.mult, fOSC, target_freq, out_D));
+ // P is introduced in Section 7.3.12 - calculate P with adaptation of
+ // Equation (3). It also comes up again in 8.1.6, although it's not
+ // called P there any more. There, it is the factor between N' and N.
+ // P == 2 whenever we're in a sync category where we need to program the
+ // N-divider with half the "normal" values. In TICS PRO, this value is
+ // described as "Calculated Included Channel Divide".
+
+ // 3. Calculate N, PLL_NUM and PLL_DEN
+ const double delta_fVCO = spur_dodging ? 2e6 : 1.0;
+ // In the next statement, we:
+ // - Estimate PLL_DEN by PLL_DEN = ceil(fPD * p / delta_fVCO)
+ // - This value can exceed the limits of uint32_t, so we clamp it between
+ // 1 and 0xFFFFFFFF (the denominator can also not be zero, so we need
+ // to catch rounding errors)
+ // - Finally, convert to uint32_t
+ const uint32_t PLL_DEN = static_cast<uint32_t>(std::max(1.0,
+ std::min(std::ceil(fPD * p / delta_fVCO),
+ double(std::numeric_limits<uint32_t>::max()))));
+ UHD_ASSERT_THROW(PLL_DEN > 0);
+ // This is where we do the N=N'/2 division from Section 8.1.6:
+ const double N_real = fVCO / (fPD * p);
+ const uint32_t N = static_cast<uint32_t>(std::floor(N_real));
+ const uint32_t PLL_NUM = std::round((N_real - double(N)) * PLL_DEN);
+
+ // See datasheet (Section 7.3.4):
+ // Equation (2): fVCO = fPD * [PLL_N + (PLL_NUM / PLL_DEN)] * p
+ // Note that p here is the "extra divider in SYNC mode" that is in the
+ // text, but not listed in Eq. (2) in this section.
+ const double fVCO_actual = fPD * p * (N + (static_cast<double>(PLL_NUM) / PLL_DEN));
+ UHD_ASSERT_THROW(3200e6 <= fVCO_actual && fVCO_actual <= 6400e6);
+ const double actual_freq = fVCO_actual / out_D;
+ // clang-format off
+ UHD_LOG_TRACE(LOG_ID,
+ "Calculating settings for fTARGET=" << (target_freq / 1e6)
+ << " MHz, fOSC=" << (fOSC / 1e6)
+ << " MHz: Target fVCO=" << (fVCO / 1e6)
+ << " MHz, actual fVCO=" << (fVCO_actual / 1e6)
+ << " MHz. R_pre=" << _regs.pll_r_pre
+ << " OSC2X=" << _regs.osc_2x
+ << " MULT=" << std::to_string(_regs.mult)
+ << " PLL_R=" << std::to_string(_regs.pll_r)
+ << " P=" << p
+ << " N=" << N
+ << " PLL_DEN=" << PLL_DEN
+ << " PLL_NUM=" << PLL_NUM
+ << " CHDIV=" << out_D);
+ // clang-format on
+
+ // 4. Set frequency dependent registers
+ _compute_and_set_mult_hi(fOSC);
+ _set_pll_n(N); // Note: N-divider values already account for
+ _set_pll_num(PLL_NUM); // N-divider adaptations at this point. No more
+ _set_pll_den(PLL_DEN); // divide-by-2 necessary.
+ _set_fcal_hpfd_adj(fPD);
+ _set_fcal_lpfd_adj(fPD);
+ _set_pfd_dly(fVCO_actual);
+ _set_mash_seed(spur_dodging, PLL_NUM, fPD);
+
+ if (get_sync_mode()) {
+ // From R69 register field description (Table 77):
+ // The delay should be at least 4 times the PLL lock time. The
+ // delay is expressed in state machine clock periods where
+ // state_machine_clock_period = 2^(CAL_CLK_DIV) / fOSC and
+ // CAL_CLK_DIV is one of {0, 1}
+ const double period = ((_regs.cal_clk_div == 0) ? 1 : 2) / fOSC;
+ const uint32_t mash_rst_count =
+ static_cast<uint32_t>(std::ceil(4 * PLL_LOCK_TIME / period));
+ _set_mash_rst_count(mash_rst_count);
+ }
+
+ // 5. Calculate charge pump gain
+ _compute_and_set_charge_pump_gain(fVCO_actual, N_real);
+
+ // 6. Calculate VCO calibration values
+ _compute_and_set_vco_cal(fVCO_actual);
+
+ // 7. Set amplitude on enabled outputs
+ if (_get_output_enabled(RF_OUTPUT_A)) {
+ _find_and_set_lo_power(actual_freq, RF_OUTPUT_A);
+ }
+ if (_get_output_enabled(RF_OUTPUT_B)) {
+ _find_and_set_lo_power(actual_freq, RF_OUTPUT_B);
+ }
+
+ return actual_freq;
+ }
+
+private:
+ /**************************************************************************
+ * Attributes
+ *************************************************************************/
+ write_fn_t _poke16;
+ read_fn_t _peek16;
+ sleep_fn_t _sleep;
+ lmx2572_regs_t _regs = lmx2572_regs_t();
+ muxout_state_t _mux_state = muxout_state_t::SDO;
+ bool _sync_mode = false;
+
+ /**************************************************************************
+ * Private Methods
+ *************************************************************************/
+ //! Identify sync category according to Section 8.1.6 of the datasheet. This
+ // function implements the flowchart (Fig. 170).
+ sync_cat _get_sync_cat(
+ const uint8_t M, const double fOSC, const double fOUT, const uint16_t CHDIV)
+ {
+ if (!get_sync_mode()) {
+ return NONE;
+ }
+ // Right-hand path of the flowchart:
+ if (M > 1) {
+ if (CHDIV > 2) {
+ return CAT4;
+ }
+ if (std::fmod(fOUT, fOSC * M) != 0) {
+ return CAT4;
+ }
+ // In the flow chart, there's a third condition (PLL_NUM == 0) but
+ // that is implied in the previous condition. Here's the proof:
+ // 1) Because of the previous condition, we know that
+ // f_OUT = f_OSC * M * K, where K is an integer.
+ // We also know that that the doubler is disabled, so we can
+ // ignore it here.
+ // 2) PLL_NUM must be zero when f_VCO / f_PD is an integer value:
+ //
+ // f_VCO
+ // N = -----
+ // f_PD
+ //
+ // 3) We can insert
+ // f_VCO = f_OUT * D
+ // and
+ // f_PD = f_OSC * M / R where R is an integer (R-divider)
+ // which yields:
+ //
+ // f_OUT * D * R
+ // N = -------------
+ // f_OSC * M
+ //
+ // 4) Now we can insert 1), which yields
+ //
+ // N = K * D * R
+ //
+ // D is either 1 or 2, and K and R are integers. Therefore, N
+ // is an integer too, and PLL_NUM == 0. _
+ // |_|
+ //
+ // Note: We could simply calculate N here, but that would require
+ // knowing K and R, which we can avoid with this simple comment.
+ }
+ // Left-hand path of the flowchart:
+ if (M == 1 && std::fmod(fOUT, fOSC) != 0) {
+ return CAT3;
+ }
+ if (M == 1 && CHDIV > 2) {
+ return CAT2;
+ }
+ if (CHDIV == 2) {
+ return CAT1B;
+ }
+ return CAT1A;
+ }
+
+ //! Enable/disable register readback mode enabled
+ // SPI MISO is multiplexed to lock detect and register readback. Reading
+ // any register when the mux is set to lock detect will return just the
+ // lock detect signal, so ensure we're in readback mode if reads desired
+ void _enable_register_readback(const bool enable)
+ {
+ auto desired_state = enable
+ ? lmx2572_regs_t::muxout_ld_sel_t::MUXOUT_LD_SEL_REGISTER_READBACK
+ : lmx2572_regs_t::muxout_ld_sel_t::MUXOUT_LD_SEL_LOCK_DETECT;
+ if (_regs.muxout_ld_sel != desired_state) {
+ _regs.muxout_ld_sel = desired_state;
+ _poke16(0, _regs.get_reg(0));
+ }
+ }
+
+ //! Sets the output divider registers
+ //
+ // Configures both the output divider and the output mux. If the divider is
+ // used, the mux input is set to CHDIV, otherwise, it's set to VCO.
+ uint16_t _set_output_divider(const double freq)
+ {
+ // clang-format off
+ // Map the desired output / LO frequency to output divider settings
+ const std::map<
+ double,
+ std::tuple<uint16_t, lmx2572_regs_t::chdiv_t>
+ > out_div_map {
+ // freq outD chdiv
+ {25e6, {256, lmx2572_regs_t::chdiv_t::CHDIV_DIVIDE_BY_256}},
+ {50e6, {128, lmx2572_regs_t::chdiv_t::CHDIV_DIVIDE_BY_128}},
+ {100e6, {64, lmx2572_regs_t::chdiv_t::CHDIV_DIVIDE_BY_64 }},
+ {200e6, {32, lmx2572_regs_t::chdiv_t::CHDIV_DIVIDE_BY_32 }},
+ {400e6, {16, lmx2572_regs_t::chdiv_t::CHDIV_DIVIDE_BY_16 }},
+ {800e6, {8, lmx2572_regs_t::chdiv_t::CHDIV_DIVIDE_BY_8 }},
+ {1.6e9, {4, lmx2572_regs_t::chdiv_t::CHDIV_DIVIDE_BY_4 }},
+ {3.2e9, {2, lmx2572_regs_t::chdiv_t::CHDIV_DIVIDE_BY_2 }},
+ // CHDIV isn't used for out_divider == 1 so use DIVIDE_BY_2
+ // We use +1 as an epsilon value here. Upon entering this function
+ // we already know that that freq <= 6.4e9. We increase the
+ // boundary here so that upper_bound() will not fail on the
+ // corner case freq == 6.4e9.
+ {6.4e9+1, {1, lmx2572_regs_t::chdiv_t::CHDIV_DIVIDE_BY_2 }}
+ };
+ // clang-format on
+
+ uint16_t out_D;
+ lmx2572_regs_t::chdiv_t chdiv;
+ auto out_div_it = out_div_map.upper_bound(freq);
+ UHD_ASSERT_THROW(out_div_it != out_div_map.end());
+ std::tie(out_D, chdiv) = out_div_it->second;
+ _regs.chdiv = lmx2572_regs_t::chdiv_t(chdiv);
+ // If we're using the output divider, map it to the corresponding output
+ // mux. Otherwise, connect the VCO directly to the mux.
+ const mux_in_t input = (out_D > 1) ? mux_in_t::DIVIDER : mux_in_t::VCO;
+ if (_get_output_enabled(RF_OUTPUT_A)) {
+ set_mux_input(RF_OUTPUT_A, input);
+ }
+ if (_get_output_enabled(RF_OUTPUT_B)) {
+ set_mux_input(RF_OUTPUT_B, input);
+ }
+
+ return out_D;
+ }
+
+ //! Returns the output enabled status of output
+ bool _get_output_enabled(const output_t output)
+ {
+ if (output == RF_OUTPUT_A) {
+ return _regs.outa_pd == lmx2572_regs_t::outa_pd_t::OUTA_PD_NORMAL_OPERATION;
+ } else {
+ return _regs.outb_pd == lmx2572_regs_t::outb_pd_t::OUTB_PD_NORMAL_OPERATION;
+ }
+ }
+
+ //! Sets the MASH_RST_COUNT registers
+ void _set_mash_rst_count(const uint32_t mash_rst_count)
+ {
+ _regs.mash_rst_count_upper = uhd::narrow_cast<uint16_t>(mash_rst_count >> 16);
+ _regs.mash_rst_count_lower = uhd::narrow_cast<uint16_t>(mash_rst_count);
+ }
+
+ //! Calculate and set the mult_hi register
+ //
+ // Sets the MULT_HI bit (needs to be high if the multiplier output frequency
+ // is larger than 100 MHz).
+ //
+ // \param ref_frequency The OSCin signal's frequency.
+ void _compute_and_set_mult_hi(const double fOSC)
+ {
+ const double fMULTout =
+ (fOSC * (int(_regs.osc_2x) + 1) * _regs.mult) / _regs.pll_r_pre;
+ _regs.mult_hi = (_regs.mult > 1 && fMULTout > 100e6)
+ ? lmx2572_regs_t::mult_hi_t::MULT_HI_GREATER_THAN_100M
+ : lmx2572_regs_t::mult_hi_t::MULT_HI_LESS_THAN_EQUAL_TO_100M;
+ }
+
+ //! Sets the mash seed value based on fPD and whether spur dodging is enabled
+ void _set_mash_seed(const bool spur_dodging, const uint32_t PLL_NUM, const double pfd)
+ {
+ uint32_t mash_seed = 0;
+ if (spur_dodging || PLL_NUM == 0) {
+ // Leave mash_seed set to 0
+ }
+ else {
+ const std::map<double, uint32_t> seed_map = {
+ {25e6, 4999},
+ {30.72e6, 5531},
+ {31.25e6, 5591},
+ {32e6, 5657},
+ {50e6, 7096},
+ {61.44e6, 7841},
+ {62.5e6, 7907},
+ {64e6, 7993}
+ };
+ mash_seed = seed_map.lower_bound(pfd)->second;
+ }
+ _regs.mash_seed_upper = uhd::narrow_cast<uint16_t>(mash_seed >> 16);
+ _regs.mash_seed_lower = uhd::narrow_cast<uint16_t>(mash_seed);
+ }
+
+ void _find_and_set_lo_power(const double freq, const output_t output)
+ {
+ if (freq < 3e9) {
+ set_output_power(output, 25);
+ } else if (3e9 <= freq && freq < 4e9) {
+ constexpr double slope = 5.0;
+ constexpr double segment_range = 1e9;
+ constexpr int power_base = 25;
+ const double offset = freq - 3e9;
+ const uint8_t power =
+ std::round<uint8_t>(power_base + ((offset / segment_range) * slope));
+ set_output_power(output, power);
+ } else if (4e9 <= freq && freq < 5e9) {
+ constexpr double slope = 10.0;
+ constexpr double segment_range = 1e9;
+ constexpr int power_base = 30;
+ const double offset = freq - 4e9;
+ const uint8_t power =
+ std::round<uint8_t>(power_base + ((offset / segment_range) * slope));
+ set_output_power(output, power);
+ } else if (5e9 <= freq && freq < 6.4e9) {
+ constexpr double slope = 5 / 1.4;
+ constexpr double segment_range = 1.4e9;
+ constexpr int power_base = 40;
+ const double offset = freq - 5e9;
+ const uint8_t power =
+ std::round<uint8_t>(power_base + ((offset / segment_range) * slope));
+ set_output_power(output, power);
+ } else if (freq >= 6.4e9) {
+ set_output_power(output, 45);
+ } else {
+ UHD_THROW_INVALID_CODE_PATH();
+ }
+ }
+
+ //! Sets the FCAL_HPFD_ADJ value based on fPD
+ void _set_fcal_hpfd_adj(const double pfd)
+ {
+ // These frequency constants are from the data sheet (Section 7.6.1)
+ if (pfd <= 37.5e6) {
+ _regs.fcal_hpfd_adj = 0x0;
+ } else if (37.5e6 < pfd && pfd <= 75e6) {
+ _regs.fcal_hpfd_adj = 0x1;
+ } else if (75e6 < pfd && pfd <= 100e6) {
+ _regs.fcal_hpfd_adj = 0x2;
+ } else { // 100 MHz > pfd
+ _regs.fcal_hpfd_adj = 0x3;
+ }
+ }
+
+ //! Sets the FCAL_LPFD_ADJ value based on the fPD (Section 7.6.1)
+ void _set_fcal_lpfd_adj(const double pfd)
+ {
+ // These frequency constants are from the data sheet (Section 7.6.1)
+ if (pfd >= 10e6) {
+ _regs.fcal_lpfd_adj = 0x0;
+ } else if (10e6 > pfd && pfd >= 5e6) {
+ _regs.fcal_lpfd_adj = 0x1;
+ } else if (5e6 > pfd && pfd >= 2.5e6) {
+ _regs.fcal_lpfd_adj = 0x2;
+ } else { // pfd > 2.5MHz
+ _regs.fcal_lpfd_adj = 0x3;
+ }
+ }
+
+ //! Sets the PFD Delay value based on fVCO (Section 7.3.4)
+ void _set_pfd_dly(const double fVCO)
+ {
+ UHD_ASSERT_THROW(_regs.mash_order == lmx2572_regs_t::MASH_ORDER_THIRD_ORDER);
+ // Thse constants / values come from the data sheet (Table 3)
+ if (3.2e9 <= fVCO && fVCO < 4e9) {
+ _regs.pfd_dly_sel = 2;
+ } else if (4e9 <= fVCO && fVCO < 4.9e9) {
+ _regs.pfd_dly_sel = 2;
+ } else if (4.9e9 <= fVCO && fVCO <= 6.4e9) {
+ _regs.pfd_dly_sel = 3;
+ } else {
+ UHD_THROW_INVALID_CODE_PATH();
+ }
+ }
+
+ //! Sets the PLL divider and multiplier values
+ void _set_pll_div_and_mult(
+ const double fTARGET, const double fVCO, const uint64_t fOSC_int)
+ {
+ // We want to avoid SYNC category 4 (device unreliable in SYNC mode) so
+ // fix the pre-divider and multiplier to 1
+ // See datasheet (Section 8.1.6)
+ _regs.pll_r_pre = 1;
+ _regs.mult = 1;
+ // Doubler fixed to disabled
+ _regs.osc_2x = lmx2572_regs_t::osc_2x_t::OSC_2X_DISABLED;
+ // Post-divider
+ uint8_t pll_r = 0;
+ // NOTE: This calculation is designed for the ZBX daughterboard. Should
+ // we want to reuse this driver elsewhere, we need to factor this out
+ // and make it a bit nicer.
+ if (get_sync_mode()) {
+ if (fTARGET < 3200e6) {
+ switch (fOSC_int) {
+ case 61440000: {
+ if (3200e6 <= fVCO && fVCO < 3950e6) {
+ pll_r = 2;
+ } else if (3950e6 <= fVCO && fVCO <= 6400e6) {
+ pll_r = 1;
+ }
+ break;
+ }
+ case 64000000: {
+ if (3200e6 <= fVCO && fVCO < 4100e6) {
+ pll_r = 2;
+ } else if (4150e6 < fVCO && fVCO <= 6400e6) {
+ pll_r = 1;
+ }
+ break;
+ }
+ case 62500000: {
+ if (3200e6 <= fVCO && fVCO < 4000e6) {
+ pll_r = 2;
+ } else if (4050e6 <= fVCO && fVCO <= 6400e6) {
+ pll_r = 1;
+ }
+ break;
+ }
+ case 50000000: {
+ pll_r = 1;
+ break;
+ }
+ default:
+ UHD_THROW_INVALID_CODE_PATH();
+ } // end switch
+ } // end if (fTARGET < 3200e6)
+ else {
+ pll_r = 1;
+ }
+ } else {
+ pll_r = 1;
+ }
+ UHD_ASSERT_THROW(pll_r > 0);
+ _regs.pll_r = pll_r;
+ // Section 7.3.2 states to not use both the double and the multiplier
+ // (M), so let's check we're doing that
+ UHD_ASSERT_THROW(
+ _regs.mult == 1 || _regs.osc_2x == lmx2572_regs_t::osc_2x_t::OSC_2X_DISABLED);
+ }
+
+ //! Set the value of VCO_PHASE_SYNC_EN according to our sync category
+ //
+ // Assumption: outa_mux and outb_mux have already been appropriately
+ // programmed for this use case.
+ //
+ // Also calculates the P-value (see set_frequency() for more discussion on
+ // that value).
+ int _set_phase_sync(const sync_cat cat)
+ {
+ int P = 1;
+ // We always set the default value for VCO_PHASE_SYNC_EN here. Some
+ // sync categories do not, in fact, require this bit to be asserted.
+ // By resetting it here, we can exactly follow the datasheet in the
+ // following switch statement.
+ _regs.vco_phase_sync_en =
+ lmx2572_regs_t::vco_phase_sync_en_t::VCO_PHASE_SYNC_EN_NORMAL_OPERATION;
+ // This switch statement implements Table 137 from Section 8.1.6 of the
+ // datasheet.
+ switch (cat) {
+ case CAT1A:
+ UHD_LOG_TRACE(LOG_ID, "Sync Category: 1A");
+ // Nothing required in this mode, input and output are always
+ // at a deterministic phase relationship.
+ break;
+ case CAT1B:
+ UHD_LOG_TRACE(LOG_ID, "Sync Category: 1B");
+ // Set VCO_PHASE_SYNC_EN = 1
+ _regs.vco_phase_sync_en = lmx2572_regs_t::vco_phase_sync_en_t::
+ VCO_PHASE_SYNC_EN_PHASE_SYNC_MODE;
+ P = 2;
+ break;
+ case CAT2:
+ UHD_LOG_TRACE(LOG_ID, "Sync Category: 2");
+ // Note: We assume the existence and usage of the SYNC pin here.
+ // This means there are no more steps required (Steps 3-6 are
+ // skipped).
+ break;
+ case CAT3:
+ UHD_LOG_TRACE(LOG_ID, "Sync Category: 3");
+ // In this category, we assume that the SYNC signal will be
+ // applied afterwards, and that timing requirements are met.
+ if (_regs.outa_mux == lmx2572_regs_t::outa_mux_t::OUTA_MUX_CHANNEL_DIVIDER
+ || _regs.outb_mux
+ == lmx2572_regs_t::outb_mux_t::OUTB_MUX_CHANNEL_DIVIDER) {
+ P = 2;
+ }
+ _regs.vco_phase_sync_en = lmx2572_regs_t::vco_phase_sync_en_t::
+ VCO_PHASE_SYNC_EN_PHASE_SYNC_MODE;
+ break;
+ case CAT4:
+ UHD_LOG_TRACE(LOG_ID, "Sync Category: 4");
+ UHD_LOG_WARNING(LOG_ID,
+ "PLL programming does not allow reliable phase synchronization!");
+ break;
+ case NONE:
+ // No phase sync, we're done
+ break;
+ default:
+ UHD_THROW_INVALID_CODE_PATH();
+ }
+ return P;
+ }
+
+ //! Compute and set charge pump gain register
+ // TODO: Charge pump settings will eventually come from a
+ // lookup table in the Cal EEPROM for Charge Pump setting vs. F_CORE VCO_.
+ void _compute_and_set_charge_pump_gain(const double fVCO_actual, const double N_real)
+ {
+ // clang-format off
+ // Table 135 (VCO Gain)
+ const std::map<
+ double,
+ std::tuple<double, double, uint8_t, uint8_t, uint8_t>
+ > vco_gain_map {
+ // fmax fmin fmax vco kmin kmax
+ {3.65e9, {3.2e9, 3.65e9, 1, 32, 47}},
+ {4.2e9, {3.65e9, 4.2e9, 2, 35, 54}},
+ {4.65e9, {4.2e9, 4.65e9, 3, 47, 64}},
+ {5.2e9, {4.65e9, 5.2e9, 4, 50, 73}},
+ {5.75e9, {5.2e9, 5.75e9, 5, 61, 82}},
+ {6.4e9, {5.75e9, 6.4e9, 6, 57, 79}}
+ };
+ // clang-format on
+ double fmin, fmax;
+ int VCO_CORE;
+ int KvcoMin, KvcoMax;
+ auto vco_gain_it = vco_gain_map.lower_bound(fVCO_actual);
+ UHD_ASSERT_THROW(vco_gain_it != vco_gain_map.end());
+ std::tie(fmin, fmax, VCO_CORE, KvcoMin, KvcoMax) = vco_gain_it->second;
+ double Kvco = uhd::math::linear_interp<double>(fVCO_actual, fmin, KvcoMin, fmax, KvcoMax);
+
+ // Calculate the optimal charge pump current (uA)
+ const double icp = 2 * uhd::math::PI * TARGET_LOOP_BANDWIDTH * N_real / (Kvco * LOOP_GAIN_SETTING_RESISTANCE);
+
+ // clang-format off
+ // Table 2 (Charge Pump Gain)
+ const std::map<double, uint8_t> cpg_map = {
+ // gain cpg
+ { 0, 0},
+ { 625, 1},
+ {1250, 2},
+ {1875, 3},
+ {2500, 4},
+ {3125, 5},
+ {3750, 6},
+ {4375, 7},
+ {5000, 12},
+ {5625, 13},
+ {6250, 14},
+ {6875, 15}
+ };
+ // clang-format on
+ const uint8_t cpg = uhd::math::at_nearest(cpg_map, icp);
+ _regs.cpg = cpg;
+ }
+
+ //! Compute and set VCO calibration values
+ // This method implements VCO partial assist calibration
+ // See datasheet (Section 8.1.4.1)
+ void _compute_and_set_vco_cal(const double fVCO_actual)
+ {
+ // clang-format off
+ // Table 136
+ const std::map<
+ double,
+ std::tuple<double, double, uint8_t, uint8_t, uint8_t, uint16_t, uint16_t>
+ > vco_partial_assist_map{
+ // fmax fmin fmax vco Cmin Cmax Amin Amax
+ {3.65e9, {3.2e9, 3.65e9, 1, 131, 19, 138, 137}},
+ {4.2e9, {3.65e9, 4.2e9, 2, 143, 25, 162, 142}},
+ {4.65e9, {4.2e9, 4.65e9, 3, 135, 34, 126, 114}},
+ {5.2e9, {4.65e9, 5.2e9, 4, 136, 25, 195, 172}},
+ {5.75e9, {5.2e9, 5.75e9, 5, 133, 20, 190, 163}},
+ {6.4e9, {5.75e9, 6.4e9, 6, 151, 27, 256, 204}}
+ };
+ // clang-format on
+ double fmin, fmax;
+ uint8_t VCO_CORE, Cmin, Cmax;
+ uint16_t Amin, Amax;
+ auto vco_cal_it = vco_partial_assist_map.lower_bound(fVCO_actual);
+ UHD_ASSERT_THROW(vco_cal_it != vco_partial_assist_map.end());
+ std::tie(fmin, fmax, VCO_CORE, Cmin, Cmax, Amin, Amax) = vco_cal_it->second;
+
+ uint16_t VCO_CAPCTRL_STRT =
+ std::round(Cmin - (fVCO_actual - fmin) * (Cmin - Cmax) / (fmax - fmin));
+ // From R78 register field description (Table 86)
+ const uint16_t VCO_CAPCTRL_STRT_MAX = 183;
+ VCO_CAPCTRL_STRT = std::min(VCO_CAPCTRL_STRT_MAX, VCO_CAPCTRL_STRT);
+
+ uint16_t VCO_DACISET_STRT =
+ std::round(Amin - ((fVCO_actual - fmin) * (Amin - Amax) / (fmax - fmin)));
+ // From R17 register field description (Table 25), 9-bit register
+ const uint16_t VCO_DACISET_STRT_MAX = 511; // 0x1FF
+ VCO_DACISET_STRT = std::min(VCO_DACISET_STRT, VCO_DACISET_STRT_MAX);
+
+ _regs.vco_sel = VCO_CORE;
+ _regs.vco_capctrl_strt = VCO_CAPCTRL_STRT;
+ _regs.vco_daciset_strt = VCO_DACISET_STRT;
+ }
+
+ void _set_pll_n(const uint32_t n)
+ {
+ UHD_ASSERT_THROW((n & 0x7FFFF) == n);
+ // The regs object masks internally, this 0x7 is just for the sake of
+ // reading
+ _regs.pll_n_upper_3_bits = uhd::narrow_cast<uint16_t>((n >> 16) & 0x7);
+ _regs.pll_n_lower_16_bits = uhd::narrow_cast<uint16_t>(n);
+ }
+
+ void _set_pll_num(const uint32_t num)
+ {
+ _regs.pll_num_upper = uhd::narrow_cast<uint16_t>(num >> 16);
+ _regs.pll_num_lower = uhd::narrow_cast<uint16_t>(num);
+ }
+
+ void _set_pll_den(const uint32_t den)
+ {
+ _regs.pll_den_upper = uhd::narrow_cast<uint16_t>(den >> 16);
+ _regs.pll_den_lower = uhd::narrow_cast<uint16_t>(den);
+ }
+
+
+ // NOTE: Some of these defaults are just sensible defaults, and get
+ // overwritten as soon as anything interesting happens. Other defaults are
+ // specific to X400/ZBX. If we want to use this driver for other dboards,
+ // we should add APIs to set those other things in order not to have a
+ // leaky abstraction (we'd like to contain lmx2572_regs_t within this file).
+ void _set_defaults()
+ {
+ _regs.ramp_en = lmx2572_regs_t::ramp_en_t::RAMP_EN_NORMAL_OPERATION;
+ _regs.vco_phase_sync_en =
+ lmx2572_regs_t::vco_phase_sync_en_t::VCO_PHASE_SYNC_EN_NORMAL_OPERATION;
+ _regs.add_hold = 0;
+ _regs.out_mute = lmx2572_regs_t::out_mute_t::OUT_MUTE_MUTED;
+ _regs.fcal_hpfd_adj = 1;
+ _regs.fcal_lpfd_adj = 0;
+ _regs.fcal_en = lmx2572_regs_t::fcal_en_t::FCAL_EN_ENABLE;
+ _regs.muxout_ld_sel = lmx2572_regs_t::muxout_ld_sel_t::MUXOUT_LD_SEL_LOCK_DETECT;
+ _regs.reset = lmx2572_regs_t::reset_t::RESET_NORMAL_OPERATION;
+ _regs.powerdown = lmx2572_regs_t::powerdown_t::POWERDOWN_NORMAL_OPERATION;
+
+ _regs.cal_clk_div = 0;
+
+ _regs.ipbuf_type = lmx2572_regs_t::ipbuf_type_t::IPBUF_TYPE_DIFFERENTIAL;
+ _regs.ipbuf_term = lmx2572_regs_t::ipbuf_term_t::IPBUF_TERM_INTERNALLY_TERMINATED;
+
+ _regs.out_force = lmx2572_regs_t::out_force_t::OUT_FORCE_USE_OUT_MUTE;
+
+ // set_frequency() implements VCO Partial assist so set the correct modes of
+ // operation and some defaults (defaults will get overwritten)
+ // See datasheet (Section 8.1.4)
+ _regs.vco_daciset_force =
+ lmx2572_regs_t::vco_daciset_force_t::VCO_DACISET_FORCE_NORMAL_OPERATION;
+ _regs.vco_capctrl_force =
+ lmx2572_regs_t::vco_capctrl_force_t::VCO_CAPCTRL_FORCE_NORMAL_OPERATION;
+ _regs.vco_sel_force = lmx2572_regs_t::vco_sel_force_t::VCO_SEL_FORCE_DISABLED;
+ _regs.vco_daciset_strt = 0x096;
+ _regs.vco_sel = 0x6;
+ _regs.vco_capctrl_strt = 0;
+
+ _regs.mult_hi = lmx2572_regs_t::mult_hi_t::MULT_HI_LESS_THAN_EQUAL_TO_100M;
+ _regs.osc_2x = lmx2572_regs_t::osc_2x_t::OSC_2X_DISABLED;
+
+ _regs.mult = 1;
+
+ _regs.pll_r = 1;
+
+ _regs.pll_r_pre = 1;
+
+ _regs.cpg = 7;
+
+ _regs.pll_n_upper_3_bits = 0;
+ _regs.pll_n_lower_16_bits = 0x28;
+
+ _regs.mash_seed_en = lmx2572_regs_t::mash_seed_en_t::MASH_SEED_EN_ENABLED;
+ _regs.pfd_dly_sel = 0x2;
+
+ _regs.pll_den_upper = 0;
+ _regs.pll_den_lower = 0;
+
+ _regs.mash_seed_upper = 0;
+ _regs.mash_seed_lower = 0;
+
+ _regs.pll_num_upper = 0;
+ _regs.pll_num_lower = 0;
+
+ _regs.outa_pwr = 0;
+ _regs.outb_pd = lmx2572_regs_t::outb_pd_t::OUTB_PD_POWER_DOWN;
+ _regs.outa_pd = lmx2572_regs_t::outa_pd_t::OUTA_PD_POWER_DOWN;
+ _regs.mash_reset_n =
+ lmx2572_regs_t::mash_reset_n_t::MASH_RESET_N_NORMAL_OPERATION;
+ _regs.mash_order = lmx2572_regs_t::mash_order_t::MASH_ORDER_THIRD_ORDER;
+
+ _regs.outa_mux = lmx2572_regs_t::outa_mux_t::OUTA_MUX_VCO;
+ _regs.outb_pwr = 0;
+
+ _regs.outb_mux = lmx2572_regs_t::outb_mux_t::OUTB_MUX_VCO;
+
+ _regs.inpin_ignore = 0;
+ _regs.inpin_hyst = lmx2572_regs_t::inpin_hyst_t::INPIN_HYST_DISABLED;
+ _regs.inpin_lvl = lmx2572_regs_t::inpin_lvl_t::INPIN_LVL_INVALID;
+ _regs.inpin_fmt =
+ lmx2572_regs_t::inpin_fmt_t::INPIN_FMT_SYNC_EQUALS_SYSREFREQ_EQUALS_CMOS2;
+
+ _regs.ld_type = lmx2572_regs_t::ld_type_t::LD_TYPE_VTUNE_AND_VCOCAL;
+
+ _regs.ld_dly = 100;
+ // See Table 144 (LDO_DLY Setting)
+ _regs.ldo_dly = 3;
+
+ _regs.dblbuf_en_0 = lmx2572_regs_t::dblbuf_en_0_t::DBLBUF_EN_0_ENABLED;
+ _regs.dblbuf_en_1 = lmx2572_regs_t::dblbuf_en_1_t::DBLBUF_EN_1_ENABLED;
+ _regs.dblbuf_en_2 = lmx2572_regs_t::dblbuf_en_2_t::DBLBUF_EN_2_ENABLED;
+ _regs.dblbuf_en_3 = lmx2572_regs_t::dblbuf_en_3_t::DBLBUF_EN_3_ENABLED;
+ _regs.dblbuf_en_4 = lmx2572_regs_t::dblbuf_en_4_t::DBLBUF_EN_4_ENABLED;
+ _regs.dblbuf_en_5 = lmx2572_regs_t::dblbuf_en_5_t::DBLBUF_EN_5_ENABLED;
+
+ _regs.mash_rst_count_upper = 0;
+ _regs.mash_rst_count_lower = 0;
+
+ // Always gets set by set_frequency()
+ _regs.chdiv = lmx2572_regs_t::chdiv_t::CHDIV_DIVIDE_BY_2;
+
+ _regs.ramp_thresh_33rd = 0;
+ _regs.quick_recal_en = 0;
+
+ _regs.ramp_thresh_upper = 0;
+ _regs.ramp_thresh_lower = 0;
+
+ _regs.ramp_limit_high_33rd = 0;
+ _regs.ramp_limit_high_upper = 0;
+ _regs.ramp_limit_high_lower = 0;
+
+ _regs.ramp_limit_low_33rd = 0;
+ _regs.ramp_limit_low_upper = 0;
+ _regs.ramp_limit_low_lower = 0;
+
+ // Per the datasheet, the following fields need to be programmed to specific
+ // constants which differ from the defaults after a reset occurs
+ _regs.reg29_reserved0 = 0;
+ _regs.reg30_reserved0 = 0x18A6;
+ _regs.reg52_reserved0 = 0x421;
+ _regs.reg57_reserved0 = 0x20;
+ _regs.reg78_reserved0 = 1;
+ }
+};
+
+lmx2572_iface::sptr lmx2572_iface::make(lmx2572_iface::write_fn_t&& poke_fn,
+ lmx2572_iface::read_fn_t&& peek_fn,
+ lmx2572_iface::sleep_fn_t&& sleep_fn)
+{
+ return std::make_shared<lmx2572_impl>(
+ std::move(poke_fn), std::move(peek_fn), std::move(sleep_fn));
+}
diff --git a/host/lib/usrp/dboard/CMakeLists.txt b/host/lib/usrp/dboard/CMakeLists.txt
index 2dd4e7e26..1d31d6930 100644
--- a/host/lib/usrp/dboard/CMakeLists.txt
+++ b/host/lib/usrp/dboard/CMakeLists.txt
@@ -49,3 +49,10 @@ 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)
+
+if(ENABLE_X400)
+ INCLUDE_SUBDIRECTORY(zbx)
+endif(ENABLE_X400)
diff --git a/host/lib/usrp/dboard/zbx/CMakeLists.txt b/host/lib/usrp/dboard/zbx/CMakeLists.txt
new file mode 100644
index 000000000..4a4a39d56
--- /dev/null
+++ b/host/lib/usrp/dboard/zbx/CMakeLists.txt
@@ -0,0 +1,17 @@
+#
+# Copyright 2019 Ettus Research, a National Instruments Brand
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+
+IF(ENABLE_X400)
+ LIST(APPEND X410_SOURCES
+ ${CMAKE_CURRENT_SOURCE_DIR}/zbx_dboard.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/zbx_dboard_init.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/zbx_cpld_ctrl.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/zbx_lo_ctrl.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/zbx_expert.cpp
+ )
+ LIBUHD_APPEND_SOURCES(${X410_SOURCES})
+ENDIF(ENABLE_X400)
+
diff --git a/host/lib/usrp/dboard/zbx/zbx_cpld_ctrl.cpp b/host/lib/usrp/dboard/zbx/zbx_cpld_ctrl.cpp
new file mode 100644
index 000000000..8899f2a18
--- /dev/null
+++ b/host/lib/usrp/dboard/zbx/zbx_cpld_ctrl.cpp
@@ -0,0 +1,931 @@
+//
+// Copyright 2020 Ettus Research, a National Instruments Brand
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+#include <uhd/utils/log.hpp>
+#include <uhdlib/usrp/dboard/zbx/zbx_cpld_ctrl.hpp>
+#include <chrono>
+#include <map>
+#include <thread>
+
+namespace {
+//! The time we need to wait after sending a SPI command
+const uhd::time_spec_t SPI_THROTTLE_TIME = uhd::time_spec_t(2e-6);
+} // namespace
+
+namespace uhd { namespace usrp { namespace zbx {
+
+// clang-format off
+const std::unordered_map<size_t, std::unordered_map<zbx_cpld_ctrl::dsa_type, zbx_cpld_regs_t::zbx_cpld_field_t>>
+ RX_DSA_CPLD_MAP
+{
+ {0, {
+ {zbx_cpld_ctrl::dsa_type::DSA1, zbx_cpld_regs_t::zbx_cpld_field_t::RX0_DSA1},
+ {zbx_cpld_ctrl::dsa_type::DSA2, zbx_cpld_regs_t::zbx_cpld_field_t::RX0_DSA2},
+ {zbx_cpld_ctrl::dsa_type::DSA3A, zbx_cpld_regs_t::zbx_cpld_field_t::RX0_DSA3_A},
+ {zbx_cpld_ctrl::dsa_type::DSA3B, zbx_cpld_regs_t::zbx_cpld_field_t::RX0_DSA3_B}
+ }},
+ {1, {
+ {zbx_cpld_ctrl::dsa_type::DSA1, zbx_cpld_regs_t::zbx_cpld_field_t::RX1_DSA1},
+ {zbx_cpld_ctrl::dsa_type::DSA2, zbx_cpld_regs_t::zbx_cpld_field_t::RX1_DSA2},
+ {zbx_cpld_ctrl::dsa_type::DSA3A, zbx_cpld_regs_t::zbx_cpld_field_t::RX1_DSA3_A},
+ {zbx_cpld_ctrl::dsa_type::DSA3B, zbx_cpld_regs_t::zbx_cpld_field_t::RX1_DSA3_B}
+ }}
+};
+
+const std::unordered_map<size_t, std::unordered_map<zbx_cpld_ctrl::dsa_type, zbx_cpld_regs_t::zbx_cpld_field_t>>
+ TX_DSA_CPLD_MAP
+{
+ {0, {
+ {zbx_cpld_ctrl::dsa_type::DSA1, zbx_cpld_regs_t::zbx_cpld_field_t::TX0_DSA1},
+ {zbx_cpld_ctrl::dsa_type::DSA2, zbx_cpld_regs_t::zbx_cpld_field_t::TX0_DSA2}
+ }},
+ {1, {
+ {zbx_cpld_ctrl::dsa_type::DSA1, zbx_cpld_regs_t::zbx_cpld_field_t::TX1_DSA1},
+ {zbx_cpld_ctrl::dsa_type::DSA2, zbx_cpld_regs_t::zbx_cpld_field_t::TX1_DSA2}
+ }}
+};
+// clang-format on
+
+
+const std::unordered_map<std::string, zbx_cpld_ctrl::dsa_type> zbx_cpld_ctrl::dsa_map{
+ {ZBX_GAIN_STAGE_DSA1, zbx_cpld_ctrl::dsa_type::DSA1},
+ {ZBX_GAIN_STAGE_DSA2, zbx_cpld_ctrl::dsa_type::DSA2},
+ {ZBX_GAIN_STAGE_DSA3A, zbx_cpld_ctrl::dsa_type::DSA3A},
+ {ZBX_GAIN_STAGE_DSA3B, zbx_cpld_ctrl::dsa_type::DSA3B}};
+
+zbx_cpld_ctrl::zbx_cpld_ctrl(poke_fn_type&& poke_fn,
+ peek_fn_type&& peek_fn,
+ sleep_fn_type&& sleep_fn,
+ const std::string& log_id)
+ : _poke32(std::move(poke_fn))
+ , _peek32(std::move(peek_fn))
+ , _sleep(std::move(sleep_fn))
+ , _lo_spi_offset(_regs.get_addr("SPI_READY"))
+ , _log_id(log_id)
+{
+ UHD_LOG_TRACE(_log_id, "Entering CPLD ctor...");
+ // Reset and stash the regs state. We can't assume the defaults in
+ // gen_zbx_cpld_regs.py match what's on the hardware.
+ commit(NO_CHAN, true);
+ _regs.save_state();
+}
+
+void zbx_cpld_ctrl::set_scratch(const uint32_t value)
+{
+ _regs.SCRATCH = value;
+ commit(NO_CHAN);
+}
+
+uint32_t zbx_cpld_ctrl::get_scratch()
+{
+ return _peek32(_regs.get_addr("SCRATCH"));
+}
+
+void zbx_cpld_ctrl::set_atr_mode(
+ const size_t channel, const atr_mode_target target, const atr_mode mode)
+{
+ UHD_ASSERT_THROW(channel == 0 || channel == 1);
+ if (target == atr_mode_target::DSA) {
+ if (channel == 0) {
+ _regs.RF0_DSA_OPTION = static_cast<zbx_cpld_regs_t::RF0_DSA_OPTION_t>(mode);
+ } else {
+ _regs.RF1_DSA_OPTION = static_cast<zbx_cpld_regs_t::RF1_DSA_OPTION_t>(mode);
+ }
+ } else {
+ if (channel == 0) {
+ _regs.RF0_OPTION = static_cast<zbx_cpld_regs_t::RF0_OPTION_t>(mode);
+ } else {
+ _regs.RF1_OPTION = static_cast<zbx_cpld_regs_t::RF1_OPTION_t>(mode);
+ }
+ }
+ commit(channel == 0 ? CHAN0 : CHAN1);
+}
+
+void zbx_cpld_ctrl::set_sw_config(
+ const size_t channel, const atr_mode_target target, const uint8_t rf_config)
+{
+ UHD_ASSERT_THROW(channel == 0 || channel == 1);
+ // clang-format off
+ static const std::map<std::pair<size_t, atr_mode_target>, zbx_cpld_regs_t::zbx_cpld_field_t>
+ mode_map{
+ {{0, atr_mode_target::PATH_LED}, zbx_cpld_regs_t::zbx_cpld_field_t::SW_RF0_CONFIG },
+ {{1, atr_mode_target::PATH_LED}, zbx_cpld_regs_t::zbx_cpld_field_t::SW_RF1_CONFIG },
+ {{0, atr_mode_target::DSA }, zbx_cpld_regs_t::zbx_cpld_field_t::SW_RF0_DSA_CONFIG},
+ {{1, atr_mode_target::DSA }, zbx_cpld_regs_t::zbx_cpld_field_t::SW_RF1_DSA_CONFIG}
+ };
+ // clang-format on
+ _regs.set_field(mode_map.at({channel, target}), rf_config);
+ commit(channel == 0 ? CHAN0 : CHAN1);
+}
+
+uint8_t zbx_cpld_ctrl::get_current_config(
+ const size_t channel, const atr_mode_target target)
+{
+ UHD_ASSERT_THROW(channel == 0 || channel == 1);
+ const uint16_t addr = _regs.get_addr("CURRENT_RF0_CONFIG");
+ const uint32_t config_reg = _peek32(addr);
+ _regs.set_reg(addr, config_reg);
+ _regs.save_state();
+ // clang-format off
+ static const std::map<std::pair<size_t, atr_mode_target>, zbx_cpld_regs_t::zbx_cpld_field_t>
+ mode_map{
+ {{0, atr_mode_target::PATH_LED}, zbx_cpld_regs_t::zbx_cpld_field_t::CURRENT_RF0_CONFIG },
+ {{1, atr_mode_target::PATH_LED}, zbx_cpld_regs_t::zbx_cpld_field_t::CURRENT_RF1_CONFIG },
+ {{0, atr_mode_target::DSA}, zbx_cpld_regs_t::zbx_cpld_field_t::CURRENT_RF0_DSA_CONFIG},
+ {{1, atr_mode_target::DSA}, zbx_cpld_regs_t::zbx_cpld_field_t::CURRENT_RF1_DSA_CONFIG}
+ };
+ // clang-format on
+ return _regs.get_field(mode_map.at({channel, target}));
+}
+
+void zbx_cpld_ctrl::set_tx_gain_switches(
+ const size_t channel, const uint8_t idx, const tx_dsa_type& dsa_steps)
+{
+ UHD_ASSERT_THROW(channel < ZBX_NUM_CHANS);
+
+ UHD_LOG_TRACE(_log_id,
+ "Set TX DSA for channel " << channel << ": DSA1=" << dsa_steps[0] << ", DSA2="
+ << dsa_steps[1] << ", AMP=" << dsa_steps[2]);
+ if (channel == 0) {
+ _regs.TX0_DSA1[idx] = dsa_steps[0];
+ _regs.TX0_DSA2[idx] = dsa_steps[1];
+ } else if (channel == 1) {
+ _regs.TX1_DSA1[idx] = dsa_steps[0];
+ _regs.TX1_DSA2[idx] = dsa_steps[1];
+ }
+ // Correct amp path gets configured by switch_tx_antenna_switches()
+ commit(channel == 0 ? CHAN0 : CHAN1);
+}
+
+void zbx_cpld_ctrl::set_rx_gain_switches(
+ const size_t channel, const uint8_t idx, const rx_dsa_type& dsa_steps)
+{
+ UHD_LOG_TRACE(_log_id,
+ "Setting RX DSA for channel "
+ << channel << ": DSA1=" << dsa_steps[0] << ", DSA2=" << dsa_steps[1]
+ << ", DSA3A=" << dsa_steps[2] << ", DSA3B=" << dsa_steps[3]);
+ if (channel == 0) {
+ _regs.RX0_DSA1[idx] = dsa_steps[0];
+ _regs.RX0_DSA2[idx] = dsa_steps[1];
+ _regs.RX0_DSA3_A[idx] = dsa_steps[2];
+ _regs.RX0_DSA3_B[idx] = dsa_steps[3];
+ } else if (channel == 1) {
+ _regs.RX1_DSA1[idx] = dsa_steps[0];
+ _regs.RX1_DSA2[idx] = dsa_steps[1];
+ _regs.RX1_DSA3_A[idx] = dsa_steps[2];
+ _regs.RX1_DSA3_B[idx] = dsa_steps[3];
+ }
+ commit(channel == 0 ? CHAN0 : CHAN1);
+}
+
+void zbx_cpld_ctrl::set_rx_gain_switches(
+ const size_t channel, const uint8_t idx, const uint8_t table_idx)
+{
+ UHD_ASSERT_THROW(channel < ZBX_NUM_CHANS);
+ UHD_LOG_TRACE(_log_id,
+ "Setting RX DSA for channel " << channel << " from table index " << table_idx);
+ if (channel == 0) {
+ _regs.RX0_TABLE_SELECT[idx] = table_idx;
+ } else {
+ _regs.RX1_TABLE_SELECT[idx] = table_idx;
+ }
+ commit(channel == 0 ? CHAN0 : CHAN1);
+}
+
+void zbx_cpld_ctrl::set_tx_gain_switches(
+ const size_t channel, const uint8_t idx, const uint8_t table_idx)
+{
+ UHD_ASSERT_THROW(channel < ZBX_NUM_CHANS);
+ UHD_LOG_TRACE(_log_id,
+ "Setting TX DSA for channel " << channel << " from table index " << table_idx);
+ if (channel == 0) {
+ _regs.TX0_TABLE_SELECT[idx] = table_idx;
+ } else {
+ _regs.TX1_TABLE_SELECT[idx] = table_idx;
+ }
+ commit(channel == 0 ? CHAN0 : CHAN1);
+}
+
+uint8_t zbx_cpld_ctrl::set_tx_dsa(
+ const size_t channel, const uint8_t idx, const dsa_type tx_dsa, const uint8_t att)
+{
+ UHD_ASSERT_THROW(channel == 0 || channel == 1);
+ UHD_ASSERT_THROW(tx_dsa == dsa_type::DSA1 || tx_dsa == dsa_type::DSA2);
+ const uint8_t att_coerced = std::min(att, ZBX_TX_DSA_MAX_ATT);
+ _regs.set_field(TX_DSA_CPLD_MAP.at(channel).at(tx_dsa), att_coerced, idx);
+ commit(channel == 0 ? CHAN0 : CHAN1);
+ return att_coerced;
+}
+
+uint8_t zbx_cpld_ctrl::set_rx_dsa(
+ const size_t channel, const uint8_t idx, const dsa_type rx_dsa, const uint8_t att)
+{
+ UHD_ASSERT_THROW(channel == 0 || channel == 1);
+ const uint8_t att_coerced = std::min(att, ZBX_RX_DSA_MAX_ATT);
+ _regs.set_field(RX_DSA_CPLD_MAP.at(channel).at(rx_dsa), att_coerced, idx);
+ commit(channel == 0 ? CHAN0 : CHAN1);
+ return att_coerced;
+}
+
+uint8_t zbx_cpld_ctrl::get_tx_dsa(const size_t channel,
+ const uint8_t idx,
+ const dsa_type tx_dsa,
+ const bool update_cache)
+{
+ UHD_ASSERT_THROW(channel == 0 || channel == 1);
+ UHD_ASSERT_THROW(tx_dsa == dsa_type::DSA1 || tx_dsa == dsa_type::DSA2);
+ if (update_cache) {
+ update_field(TX_DSA_CPLD_MAP.at(channel).at(tx_dsa), idx);
+ }
+ return _regs.get_field(TX_DSA_CPLD_MAP.at(channel).at(tx_dsa), idx);
+}
+
+uint8_t zbx_cpld_ctrl::get_rx_dsa(const size_t channel,
+ const uint8_t idx,
+ const dsa_type rx_dsa,
+ const bool update_cache)
+{
+ UHD_ASSERT_THROW(channel == 0 || channel == 1);
+ if (update_cache) {
+ update_field(RX_DSA_CPLD_MAP.at(channel).at(rx_dsa), idx);
+ }
+ return _regs.get_field(RX_DSA_CPLD_MAP.at(channel).at(rx_dsa), idx);
+}
+
+void zbx_cpld_ctrl::set_tx_antenna_switches(
+ const size_t channel, const uint8_t idx, const std::string& antenna, const tx_amp amp)
+{
+ UHD_ASSERT_THROW(channel < ZBX_NUM_CHANS);
+ UHD_ASSERT_THROW(
+ amp == tx_amp::BYPASS || amp == tx_amp::LOWBAND || amp == tx_amp::HIGHBAND);
+
+ // Antenna settings: TX/RX, CAL_LOOPBACK
+ if (channel == 0) {
+ if (antenna == ANTENNA_TXRX) {
+ // clang-format off
+ static const std::map<tx_amp,
+ std::pair<zbx_cpld_regs_t::TX0_ANT_11_t, zbx_cpld_regs_t::TX0_ANT_10_t>> amp_map{
+ {tx_amp::BYPASS, {zbx_cpld_regs_t::TX0_ANT_11_BYPASS_AMP, zbx_cpld_regs_t::TX0_ANT_10_BYPASS_AMP}},
+ {tx_amp::LOWBAND, {zbx_cpld_regs_t::TX0_ANT_11_LOWBAND_AMP, zbx_cpld_regs_t::TX0_ANT_10_LOWBAND_AMP}},
+ {tx_amp::HIGHBAND, {zbx_cpld_regs_t::TX0_ANT_11_HIGHBAND_AMP, zbx_cpld_regs_t::TX0_ANT_10_HIGHBAND_AMP}}
+ };
+ // clang-format on
+ std::tie(_regs.TX0_ANT_11[idx], _regs.TX0_ANT_10[idx]) = amp_map.at(amp);
+ } else if (antenna == ANTENNA_CAL_LOOPBACK) {
+ _regs.TX0_ANT_10[idx] = zbx_cpld_regs_t::TX0_ANT_10_CAL_LOOPBACK;
+ _regs.RX0_ANT_1[idx] = zbx_cpld_regs_t::RX0_ANT_1_CAL_LOOPBACK;
+ _regs.TX0_ANT_11[idx] = zbx_cpld_regs_t::TX0_ANT_11_BYPASS_AMP;
+ } else {
+ UHD_LOG_WARNING(_log_id,
+ "ZBX Radio: TX Antenna setting not recognized: \"" << antenna.c_str()
+ << "\"");
+ }
+ } else {
+ // Antenna settings: TX/RX, CAL_LOOPBACK
+ if (antenna == ANTENNA_TXRX) {
+ // clang-format off
+ static const std::map<tx_amp,
+ std::pair<zbx_cpld_regs_t::TX1_ANT_11_t, zbx_cpld_regs_t::TX1_ANT_10_t>> amp_map{
+ {tx_amp::BYPASS, {zbx_cpld_regs_t::TX1_ANT_11_BYPASS_AMP, zbx_cpld_regs_t::TX1_ANT_10_BYPASS_AMP}},
+ {tx_amp::LOWBAND, {zbx_cpld_regs_t::TX1_ANT_11_LOWBAND_AMP, zbx_cpld_regs_t::TX1_ANT_10_LOWBAND_AMP}},
+ {tx_amp::HIGHBAND, {zbx_cpld_regs_t::TX1_ANT_11_HIGHBAND_AMP, zbx_cpld_regs_t::TX1_ANT_10_HIGHBAND_AMP}}
+ };
+ // clang-format on
+ std::tie(_regs.TX1_ANT_11[idx], _regs.TX1_ANT_10[idx]) = amp_map.at(amp);
+ } else if (antenna == ANTENNA_CAL_LOOPBACK) {
+ _regs.TX1_ANT_10[idx] = zbx_cpld_regs_t::TX1_ANT_10_CAL_LOOPBACK;
+ _regs.RX1_ANT_1[idx] = zbx_cpld_regs_t::RX1_ANT_1_CAL_LOOPBACK;
+ _regs.TX1_ANT_11[idx] = zbx_cpld_regs_t::TX1_ANT_11_BYPASS_AMP;
+ } else {
+ UHD_LOG_WARNING(_log_id,
+ "ZBX Radio: TX Antenna setting not recognized: \"" << antenna << "\"");
+ }
+ }
+ commit(channel == 0 ? CHAN0 : CHAN1);
+}
+
+void zbx_cpld_ctrl::set_rx_antenna_switches(
+ const size_t channel, const uint8_t idx, const std::string& antenna)
+{
+ UHD_ASSERT_THROW(channel < ZBX_NUM_CHANS);
+
+ // Antenna settings: RX2, TX/RX, CAL_LOOPBACK, TERMINATION
+ if (channel == 0) {
+ if (antenna == ANTENNA_TXRX) {
+ _regs.RX0_ANT_1[idx] = zbx_cpld_regs_t::RX0_ANT_1_TX_RX;
+ _regs.TX0_ANT_11[idx] = zbx_cpld_regs_t::TX0_ANT_11_TX_RX;
+ } else if (antenna == ANTENNA_CAL_LOOPBACK) {
+ _regs.RX0_ANT_1[idx] = zbx_cpld_regs_t::RX0_ANT_1_CAL_LOOPBACK;
+ _regs.TX0_ANT_10[idx] = zbx_cpld_regs_t::TX0_ANT_10_CAL_LOOPBACK;
+ _regs.TX0_ANT_11[idx] = zbx_cpld_regs_t::TX0_ANT_11_BYPASS_AMP;
+ } else if (antenna == ANTENNA_TERMINATION) {
+ _regs.RX0_ANT_1[idx] = zbx_cpld_regs_t::RX0_ANT_1_TERMINATION;
+ } else if (antenna == ANTENNA_RX) {
+ _regs.RX0_ANT_1[idx] = zbx_cpld_regs_t::RX0_ANT_1_RX2;
+ } else {
+ UHD_LOG_WARNING(_log_id,
+ "ZBX Radio: RX Antenna setting not recognized: \"" << antenna << "\"");
+ }
+ } else {
+ if (antenna == ANTENNA_TXRX) {
+ _regs.RX1_ANT_1[idx] = zbx_cpld_regs_t::RX1_ANT_1_TX_RX;
+ _regs.TX1_ANT_11[idx] = zbx_cpld_regs_t::TX1_ANT_11_TX_RX;
+ } else if (antenna == ANTENNA_CAL_LOOPBACK) {
+ _regs.RX1_ANT_1[idx] = zbx_cpld_regs_t::RX1_ANT_1_CAL_LOOPBACK;
+ _regs.TX1_ANT_10[idx] = zbx_cpld_regs_t::TX1_ANT_10_CAL_LOOPBACK;
+ _regs.TX1_ANT_11[idx] = zbx_cpld_regs_t::TX1_ANT_11_BYPASS_AMP;
+ } else if (antenna == ANTENNA_TERMINATION) {
+ _regs.RX1_ANT_1[idx] = zbx_cpld_regs_t::RX1_ANT_1_TERMINATION;
+ } else if (antenna == ANTENNA_RX) {
+ _regs.RX1_ANT_1[idx] = zbx_cpld_regs_t::RX1_ANT_1_RX2;
+ } else {
+ UHD_LOG_WARNING(_log_id,
+ "ZBX Radio: RX Antenna setting not recognized: \"" << antenna << "\"");
+ }
+ }
+ commit(channel == 0 ? CHAN0 : CHAN1);
+}
+
+tx_amp zbx_cpld_ctrl::get_tx_amp_settings(
+ const size_t channel, const uint8_t idx, const bool update_cache)
+{
+ if (channel == 0) {
+ if (update_cache) {
+ update_field(zbx_cpld_regs_t::zbx_cpld_field_t::TX0_ANT_10, idx);
+ update_field(zbx_cpld_regs_t::zbx_cpld_field_t::TX0_ANT_11, idx);
+ }
+ if ((_regs.TX0_ANT_11[idx] == zbx_cpld_regs_t::TX0_ANT_11_BYPASS_AMP
+ && _regs.TX0_ANT_10[idx] != zbx_cpld_regs_t::TX0_ANT_10_BYPASS_AMP)
+ || (_regs.TX0_ANT_11[idx] == zbx_cpld_regs_t::TX0_ANT_11_HIGHBAND_AMP
+ && _regs.TX0_ANT_10[idx] != zbx_cpld_regs_t::TX0_ANT_10_HIGHBAND_AMP)
+ || (_regs.TX0_ANT_11[idx] == zbx_cpld_regs_t::TX0_ANT_11_LOWBAND_AMP
+ && _regs.TX0_ANT_10[idx] != zbx_cpld_regs_t::TX0_ANT_10_LOWBAND_AMP)) {
+ UHD_LOG_WARNING(
+ _log_id, "Detected inconsistency in the TX amp switch settings.");
+ }
+ // clang-format off
+ static const std::map<zbx_cpld_regs_t::TX0_ANT_10_t, tx_amp> amp_map{
+ {zbx_cpld_regs_t::TX0_ANT_10_BYPASS_AMP , tx_amp::BYPASS },
+ {zbx_cpld_regs_t::TX0_ANT_10_CAL_LOOPBACK, tx_amp::BYPASS },
+ {zbx_cpld_regs_t::TX0_ANT_10_LOWBAND_AMP , tx_amp::LOWBAND },
+ {zbx_cpld_regs_t::TX0_ANT_10_HIGHBAND_AMP, tx_amp::HIGHBAND}
+ };
+ // clang-format on
+ return amp_map.at(_regs.TX0_ANT_10[idx]);
+ }
+ if (channel == 1) {
+ if (update_cache) {
+ update_field(zbx_cpld_regs_t::zbx_cpld_field_t::TX0_ANT_10, idx);
+ update_field(zbx_cpld_regs_t::zbx_cpld_field_t::TX0_ANT_11, idx);
+ }
+ if ((_regs.TX1_ANT_11[idx] == zbx_cpld_regs_t::TX1_ANT_11_BYPASS_AMP
+ && _regs.TX1_ANT_10[idx] != zbx_cpld_regs_t::TX1_ANT_10_BYPASS_AMP)
+ || (_regs.TX1_ANT_11[idx] == zbx_cpld_regs_t::TX1_ANT_11_HIGHBAND_AMP
+ && _regs.TX1_ANT_10[idx] != zbx_cpld_regs_t::TX1_ANT_10_HIGHBAND_AMP)
+ || (_regs.TX1_ANT_11[idx] == zbx_cpld_regs_t::TX1_ANT_11_LOWBAND_AMP
+ && _regs.TX1_ANT_10[idx] != zbx_cpld_regs_t::TX1_ANT_10_LOWBAND_AMP)) {
+ UHD_LOG_WARNING(
+ _log_id, "Detected inconsistency in the TX amp switch settings.");
+ }
+ // clang-format off
+ static const std::map<zbx_cpld_regs_t::TX1_ANT_10_t, tx_amp> amp_map{
+ {zbx_cpld_regs_t::TX1_ANT_10_BYPASS_AMP , tx_amp::BYPASS },
+ {zbx_cpld_regs_t::TX1_ANT_10_CAL_LOOPBACK, tx_amp::BYPASS },
+ {zbx_cpld_regs_t::TX1_ANT_10_LOWBAND_AMP , tx_amp::LOWBAND },
+ {zbx_cpld_regs_t::TX1_ANT_10_HIGHBAND_AMP, tx_amp::HIGHBAND}
+ };
+ // clang-format on
+ return amp_map.at(_regs.TX1_ANT_10[idx]);
+ }
+ UHD_THROW_INVALID_CODE_PATH();
+}
+
+void zbx_cpld_ctrl::set_rx_rf_filter(
+ const size_t channel, const uint8_t idx, const uint8_t rf_fir)
+{
+ UHD_ASSERT_THROW(channel < ZBX_NUM_CHANS && rf_fir < 4);
+
+ if (rf_fir == 0) {
+ if (channel == 0) {
+ _regs.RX0_4[idx] = zbx_cpld_regs_t::RX0_4_HIGHBAND;
+ _regs.RX0_2[idx] = zbx_cpld_regs_t::RX0_2_HIGHBAND;
+ } else {
+ _regs.RX1_4[idx] = zbx_cpld_regs_t::RX1_4_HIGHBAND;
+ _regs.RX1_2[idx] = zbx_cpld_regs_t::RX1_2_HIGHBAND;
+ }
+ } else {
+ // Clang-format likes to "staircase" multiple tertiary statements, it's much
+ // easier to read lined up
+ // clang-format off
+ if (channel == 0) {
+ _regs.RX0_4[idx] = zbx_cpld_regs_t::RX0_4_LOWBAND;
+ _regs.RX0_2[idx] = zbx_cpld_regs_t::RX0_2_LOWBAND;
+ _regs.RX0_RF_11[idx] = rf_fir == 1 ? zbx_cpld_regs_t::RX0_RF_11_RF_1
+ : rf_fir == 2 ? zbx_cpld_regs_t::RX0_RF_11_RF_2
+ : zbx_cpld_regs_t::RX0_RF_11_RF_3;
+ _regs.RX0_RF_3[idx] = rf_fir == 1 ? zbx_cpld_regs_t::RX0_RF_3_RF_1
+ : rf_fir == 2 ? zbx_cpld_regs_t::RX0_RF_3_RF_2
+ : zbx_cpld_regs_t::RX0_RF_3_RF_3;
+ } else {
+ _regs.RX1_4[idx] = zbx_cpld_regs_t::RX1_4_LOWBAND;
+ _regs.RX1_2[idx] = zbx_cpld_regs_t::RX1_2_LOWBAND;
+ _regs.RX1_RF_11[idx] = rf_fir == 1 ? zbx_cpld_regs_t::RX1_RF_11_RF_1
+ : rf_fir == 2 ? zbx_cpld_regs_t::RX1_RF_11_RF_2
+ : zbx_cpld_regs_t::RX1_RF_11_RF_3;
+ _regs.RX1_RF_3[idx] = rf_fir == 1 ? zbx_cpld_regs_t::RX1_RF_3_RF_1
+ : rf_fir == 2 ? zbx_cpld_regs_t::RX1_RF_3_RF_2
+ : zbx_cpld_regs_t::RX1_RF_3_RF_3;
+ }
+ // clang-format on
+ }
+ commit(channel == 0 ? CHAN0 : CHAN1);
+}
+
+void zbx_cpld_ctrl::set_rx_if1_filter(
+ const size_t channel, const uint8_t idx, const uint8_t if1_fir)
+{
+ UHD_ASSERT_THROW(channel < ZBX_NUM_CHANS && if1_fir != 0 && if1_fir < 5);
+
+ // Clang-format likes to "staircase" multiple tertiary statements, it's much
+ // easier to read lined up
+ // clang-format off
+ if (channel == 0) {
+ _regs.RX0_IF1_5[idx] = if1_fir == 1 ? zbx_cpld_regs_t::RX0_IF1_5_FILTER_1
+ : if1_fir == 2 ? zbx_cpld_regs_t::RX0_IF1_5_FILTER_2
+ : if1_fir == 3 ? zbx_cpld_regs_t::RX0_IF1_5_FILTER_3
+ : zbx_cpld_regs_t::RX0_IF1_5_FILTER_4;
+
+ _regs.RX0_IF1_6[idx] = if1_fir == 1 ? zbx_cpld_regs_t::RX0_IF1_6_FILTER_1
+ : if1_fir == 2 ? zbx_cpld_regs_t::RX0_IF1_6_FILTER_2
+ : if1_fir == 3 ? zbx_cpld_regs_t::RX0_IF1_6_FILTER_3
+ : zbx_cpld_regs_t::RX0_IF1_6_FILTER_4;
+ } else {
+ _regs.RX1_IF1_5[idx] = if1_fir == 1 ? zbx_cpld_regs_t::RX1_IF1_5_FILTER_1
+ : if1_fir == 2 ? zbx_cpld_regs_t::RX1_IF1_5_FILTER_2
+ : if1_fir == 3 ? zbx_cpld_regs_t::RX1_IF1_5_FILTER_3
+ : zbx_cpld_regs_t::RX1_IF1_5_FILTER_4;
+
+ _regs.RX1_IF1_6[idx] = if1_fir == 1 ? zbx_cpld_regs_t::RX1_IF1_6_FILTER_1
+ : if1_fir == 2 ? zbx_cpld_regs_t::RX1_IF1_6_FILTER_2
+ : if1_fir == 3 ? zbx_cpld_regs_t::RX1_IF1_6_FILTER_3
+ : zbx_cpld_regs_t::RX1_IF1_6_FILTER_4;
+ }
+ // clang-format on
+ commit(channel == 0 ? CHAN0 : CHAN1);
+}
+
+void zbx_cpld_ctrl::set_rx_if2_filter(
+ const size_t channel, const uint8_t idx, const uint8_t if2_fir)
+{
+ UHD_ASSERT_THROW(channel < ZBX_NUM_CHANS && if2_fir != 0 && if2_fir < 3);
+
+ if (channel == 0) {
+ _regs.RX0_IF2_7_8[idx] = if2_fir == 1 ? zbx_cpld_regs_t::RX0_IF2_7_8_FILTER_1
+ : zbx_cpld_regs_t::RX0_IF2_7_8_FILTER_2;
+ } else {
+ _regs.RX1_IF2_7_8[idx] = if2_fir == 1 ? zbx_cpld_regs_t::RX1_IF2_7_8_FILTER_1
+ : zbx_cpld_regs_t::RX1_IF2_7_8_FILTER_2;
+ }
+ commit(channel == 0 ? CHAN0 : CHAN1);
+}
+
+void zbx_cpld_ctrl::set_tx_rf_filter(
+ const size_t channel, const uint8_t idx, const uint8_t rf_fir)
+{
+ UHD_ASSERT_THROW(channel < ZBX_NUM_CHANS && rf_fir < 4);
+
+ if (rf_fir == 0) {
+ if (channel == 0) {
+ _regs.TX0_RF_9[idx] = zbx_cpld_regs_t::TX0_RF_9_HIGHBAND;
+ _regs.TX0_7[idx] = zbx_cpld_regs_t::TX0_7_HIGHBAND;
+ } else {
+ _regs.TX1_RF_9[idx] = zbx_cpld_regs_t::TX1_RF_9_HIGHBAND;
+ _regs.TX1_7[idx] = zbx_cpld_regs_t::TX1_7_HIGHBAND;
+ }
+ } else {
+ // Clang-format likes to "staircase" multiple tertiary statements, it's much
+ // easier to read lined up
+ // clang-format off
+ if (channel == 0) {
+ _regs.TX0_RF_9[idx] = rf_fir == 1 ? zbx_cpld_regs_t::TX0_RF_9_RF_1
+ : rf_fir == 2 ? zbx_cpld_regs_t::TX0_RF_9_RF_2
+ : zbx_cpld_regs_t::TX0_RF_9_RF_3;
+
+ _regs.TX0_RF_8[idx] = rf_fir == 1 ? zbx_cpld_regs_t::TX0_RF_8_RF_1
+ : rf_fir == 2 ? zbx_cpld_regs_t::TX0_RF_8_RF_2
+ : zbx_cpld_regs_t::TX0_RF_8_RF_3;
+ _regs.TX0_7[idx] = zbx_cpld_regs_t::TX0_7_LOWBAND;
+ } else {
+ _regs.TX1_RF_9[idx] = rf_fir == 1 ? zbx_cpld_regs_t::TX1_RF_9_RF_1
+ : rf_fir == 2 ? zbx_cpld_regs_t::TX1_RF_9_RF_2
+ : zbx_cpld_regs_t::TX1_RF_9_RF_3;
+
+ _regs.TX1_RF_8[idx] = rf_fir == 1 ? zbx_cpld_regs_t::TX1_RF_8_RF_1
+ : rf_fir == 2 ? zbx_cpld_regs_t::TX1_RF_8_RF_2
+ : zbx_cpld_regs_t::TX1_RF_8_RF_3;
+ _regs.TX1_7[idx] = zbx_cpld_regs_t::TX1_7_LOWBAND;
+ }
+ // clang-format on
+ }
+ commit(channel == 0 ? CHAN0 : CHAN1);
+}
+
+void zbx_cpld_ctrl::set_tx_if1_filter(
+ const size_t channel, const uint8_t idx, const uint8_t if1_fir)
+{
+ UHD_ASSERT_THROW(channel < ZBX_NUM_CHANS && if1_fir != 0 && if1_fir < 7);
+
+ if (if1_fir < 4) {
+ // Clang-format likes to "staircase" multiple tertiary statements, it's much
+ // easier to read lined up
+ // clang-format off
+ if (channel == 0) {
+ _regs.TX0_IF1_6[idx] = zbx_cpld_regs_t::TX0_IF1_6_FILTER_0_3;
+ _regs.TX0_IF1_3[idx] = zbx_cpld_regs_t::TX0_IF1_3_FILTER_0_3;
+ _regs.TX0_IF1_4[idx] = if1_fir == 1 ? zbx_cpld_regs_t::TX0_IF1_4_FILTER_1
+ : if1_fir == 2 ? zbx_cpld_regs_t::TX0_IF1_4_FILTER_2
+ : zbx_cpld_regs_t::TX0_IF1_4_FILTER_3;
+
+ _regs.TX0_IF1_5[idx] = if1_fir == 1 ? zbx_cpld_regs_t::TX0_IF1_5_FILTER_1
+ : if1_fir == 2 ? zbx_cpld_regs_t::TX0_IF1_5_FILTER_2
+ : zbx_cpld_regs_t::TX0_IF1_5_FILTER_3;
+ } else {
+ _regs.TX1_IF1_6[idx] = zbx_cpld_regs_t::TX1_IF1_6_FILTER_0_3;
+ _regs.TX1_IF1_3[idx] = zbx_cpld_regs_t::TX1_IF1_3_FILTER_0_3;
+ _regs.TX1_IF1_4[idx] = if1_fir == 1 ? zbx_cpld_regs_t::TX1_IF1_4_FILTER_1
+ : if1_fir == 2 ? zbx_cpld_regs_t::TX1_IF1_4_FILTER_2
+ : zbx_cpld_regs_t::TX1_IF1_4_FILTER_3;
+
+ _regs.TX1_IF1_5[idx] = if1_fir == 1 ? zbx_cpld_regs_t::TX1_IF1_5_FILTER_1
+ : if1_fir == 2 ? zbx_cpld_regs_t::TX1_IF1_5_FILTER_2
+ : zbx_cpld_regs_t::TX1_IF1_5_FILTER_3;
+ }
+ } else {
+ if (channel == 0) {
+ _regs.TX0_IF1_4[idx] = zbx_cpld_regs_t::TX0_IF1_4_TERMINATION;
+ _regs.TX0_IF1_5[idx] = zbx_cpld_regs_t::TX0_IF1_5_TERMINATION;
+ _regs.TX0_IF1_3[idx] = if1_fir == 4 ? zbx_cpld_regs_t::TX0_IF1_3_FILTER_4
+ : if1_fir == 5 ? zbx_cpld_regs_t::TX0_IF1_3_FILTER_5
+ : zbx_cpld_regs_t::TX0_IF1_3_FILTER_6;
+
+ _regs.TX0_IF1_6[idx] = if1_fir == 4 ? zbx_cpld_regs_t::TX0_IF1_6_FILTER_4
+ : if1_fir == 5 ? zbx_cpld_regs_t::TX0_IF1_6_FILTER_5
+ : zbx_cpld_regs_t::TX0_IF1_6_FILTER_6;
+ } else {
+ _regs.TX1_IF1_4[idx] = zbx_cpld_regs_t::TX1_IF1_4_TERMINATION;
+ _regs.TX1_IF1_5[idx] = zbx_cpld_regs_t::TX1_IF1_5_TERMINATION;
+ _regs.TX1_IF1_3[idx] = if1_fir == 4 ? zbx_cpld_regs_t::TX1_IF1_3_FILTER_4
+ : if1_fir == 5 ? zbx_cpld_regs_t::TX1_IF1_3_FILTER_5
+ : zbx_cpld_regs_t::TX1_IF1_3_FILTER_6;
+
+ _regs.TX1_IF1_6[idx] = if1_fir == 4 ? zbx_cpld_regs_t::TX1_IF1_6_FILTER_4
+ : if1_fir == 5 ? zbx_cpld_regs_t::TX1_IF1_6_FILTER_5
+ : zbx_cpld_regs_t::TX1_IF1_6_FILTER_6;
+ }
+ // clang-format on
+ }
+ commit(channel == 0 ? CHAN0 : CHAN1);
+}
+
+void zbx_cpld_ctrl::set_tx_if2_filter(
+ const size_t channel, const uint8_t idx, const uint8_t if2_fir)
+{
+ UHD_ASSERT_THROW(channel < ZBX_NUM_CHANS && if2_fir != 0 && if2_fir < 3);
+
+ if (channel == 0) {
+ _regs.TX0_IF2_1_2[idx] = if2_fir == 1 ? zbx_cpld_regs_t::TX0_IF2_1_2_FILTER_1
+ : zbx_cpld_regs_t::TX0_IF2_1_2_FILTER_2;
+ } else {
+ _regs.TX1_IF2_1_2[idx] = if2_fir == 1 ? zbx_cpld_regs_t::TX1_IF2_1_2_FILTER_1
+ : zbx_cpld_regs_t::TX1_IF2_1_2_FILTER_2;
+ }
+ commit(channel == 0 ? CHAN0 : CHAN1);
+}
+
+/******************************************************************************
+ * LED control
+ *****************************************************************************/
+void zbx_cpld_ctrl::set_leds(const size_t channel,
+ const uint8_t idx,
+ const bool rx,
+ const bool trx_rx,
+ const bool trx_tx)
+{
+ UHD_ASSERT_THROW(channel < ZBX_NUM_CHANS);
+ if (channel == 0) {
+ _regs.RX0_RX_LED[idx] = rx ? zbx_cpld_regs_t::RX0_RX_LED_ENABLE
+ : zbx_cpld_regs_t::RX0_RX_LED_DISABLE;
+ _regs.RX0_TRX_LED[idx] = trx_rx ? zbx_cpld_regs_t::RX0_TRX_LED_ENABLE
+ : zbx_cpld_regs_t::RX0_TRX_LED_DISABLE;
+ _regs.TX0_TRX_LED[idx] = trx_tx ? zbx_cpld_regs_t::TX0_TRX_LED_ENABLE
+ : zbx_cpld_regs_t::TX0_TRX_LED_DISABLE;
+ } else {
+ _regs.RX1_RX_LED[idx] = rx ? zbx_cpld_regs_t::RX1_RX_LED_ENABLE
+ : zbx_cpld_regs_t::RX1_RX_LED_DISABLE;
+ _regs.RX1_TRX_LED[idx] = trx_rx ? zbx_cpld_regs_t::RX1_TRX_LED_ENABLE
+ : zbx_cpld_regs_t::RX1_TRX_LED_DISABLE;
+ _regs.TX1_TRX_LED[idx] = trx_tx ? zbx_cpld_regs_t::TX1_TRX_LED_ENABLE
+ : zbx_cpld_regs_t::TX1_TRX_LED_DISABLE;
+ }
+ commit(channel == 0 ? CHAN0 : CHAN1);
+}
+
+/******************************************************************************
+ * LO control
+ *****************************************************************************/
+void zbx_cpld_ctrl::lo_poke16(const zbx_lo_t lo, const uint8_t addr, const uint16_t data)
+{
+ _lo_spi_transact(lo, addr, data, spi_xact_t::WRITE, true);
+ // We always sleep here, in the assumption that the next poke to the CPLD is
+ // also a
+ // SPI transaction.
+ // Note that this causes minor inefficiencies when stacking SPI writes with
+ // other, non-SPI pokes (because the last SPI poke will still be followed by
+ // a sleep, which isn't even necessary). If this becomes an issue, this
+ // function can be changed to include a flag as an argument whether or not
+ // to throttle.
+}
+
+uint16_t zbx_cpld_ctrl::lo_peek16(const zbx_lo_t lo, const uint8_t addr)
+{
+ _lo_spi_transact(lo, addr, 0, spi_xact_t::READ, true);
+ // Now poll the LO_SPI_READY register until we have good return value
+ const auto timeout = std::chrono::steady_clock::now()
+ + std::chrono::milliseconds(ZBX_LO_LOCK_TIMEOUT_MS);
+ while (std::chrono::steady_clock::now() < timeout) {
+ _regs.set_reg(_lo_spi_offset, _peek32(_lo_spi_offset));
+ if (_regs.DATA_VALID) {
+ break;
+ }
+ }
+
+ // Mark this register clean again
+ _regs.save_state();
+ if (!_regs.DATA_VALID) {
+ const std::string err_msg =
+ "Unable to read back from LO SPI! Transaction timed out after "
+ + std::to_string(ZBX_LO_LOCK_TIMEOUT_MS) + " ms.";
+ UHD_LOG_ERROR(_log_id, err_msg);
+ throw uhd::io_error(err_msg);
+ }
+ // The read worked. Now we run some sanity checks to make sure we got what
+ // we expected
+ UHD_ASSERT_THROW(_regs.ADDRESS == addr);
+ UHD_ASSERT_THROW(_regs.LO_SELECT == zbx_cpld_regs_t::LO_SELECT_t(lo));
+ // All good, return the read value
+ return _regs.DATA;
+}
+
+bool zbx_cpld_ctrl::lo_spi_ready()
+{
+ return _peek32(_lo_spi_offset) & (1 << 30);
+}
+
+void zbx_cpld_ctrl::set_lo_source(
+ const size_t idx, const zbx_lo_t lo, const zbx_lo_source_t lo_source)
+{
+ // LO source is either internal or external
+ const bool internal = lo_source == zbx_lo_source_t::internal;
+ switch (lo) {
+ case zbx_lo_t::TX0_LO1:
+ _regs.TX0_LO_14[idx] = internal ? zbx_cpld_regs_t::TX0_LO_14_INTERNAL
+ : zbx_cpld_regs_t::TX0_LO_14_EXTERNAL;
+ break;
+ case zbx_lo_t::TX0_LO2:
+ _regs.TX0_LO_13[idx] = internal ? zbx_cpld_regs_t::TX0_LO_13_INTERNAL
+ : zbx_cpld_regs_t::TX0_LO_13_EXTERNAL;
+ break;
+ case zbx_lo_t::TX1_LO1:
+ _regs.TX1_LO_14[idx] = internal ? zbx_cpld_regs_t::TX1_LO_14_INTERNAL
+ : zbx_cpld_regs_t::TX1_LO_14_EXTERNAL;
+ break;
+ case zbx_lo_t::TX1_LO2:
+ _regs.TX1_LO_13[idx] = internal ? zbx_cpld_regs_t::TX1_LO_13_INTERNAL
+ : zbx_cpld_regs_t::TX1_LO_13_EXTERNAL;
+ break;
+ case zbx_lo_t::RX0_LO1:
+ _regs.RX0_LO_9[idx] = internal ? zbx_cpld_regs_t::RX0_LO_9_INTERNAL
+ : zbx_cpld_regs_t::RX0_LO_9_EXTERNAL;
+ break;
+ case zbx_lo_t::RX0_LO2:
+ _regs.RX0_LO_10[idx] = internal ? zbx_cpld_regs_t::RX0_LO_10_INTERNAL
+ : zbx_cpld_regs_t::RX0_LO_10_EXTERNAL;
+ break;
+ case zbx_lo_t::RX1_LO1:
+ _regs.RX1_LO_9[idx] = internal ? zbx_cpld_regs_t::RX1_LO_9_INTERNAL
+ : zbx_cpld_regs_t::RX1_LO_9_EXTERNAL;
+ break;
+ case zbx_lo_t::RX1_LO2:
+ _regs.RX1_LO_10[idx] = internal ? zbx_cpld_regs_t::RX1_LO_10_INTERNAL
+ : zbx_cpld_regs_t::RX1_LO_10_EXTERNAL;
+ break;
+ default:
+ UHD_THROW_INVALID_CODE_PATH();
+ }
+ if (lo == zbx_lo_t::TX0_LO1 || lo == zbx_lo_t::TX0_LO2 || lo == zbx_lo_t::RX0_LO1
+ || lo == zbx_lo_t::RX0_LO2) {
+ commit(CHAN0);
+ } else {
+ commit(CHAN1);
+ }
+}
+
+zbx_lo_source_t zbx_cpld_ctrl::get_lo_source(const size_t idx, zbx_lo_t lo)
+{
+ switch (lo) {
+ case zbx_lo_t::TX0_LO1:
+ return _regs.TX0_LO_14[idx] == zbx_cpld_regs_t::TX0_LO_14_INTERNAL
+ ? zbx_lo_source_t::internal
+ : zbx_lo_source_t::external;
+ case zbx_lo_t::TX0_LO2:
+ return _regs.TX0_LO_13[idx] == zbx_cpld_regs_t::TX0_LO_13_INTERNAL
+ ? zbx_lo_source_t::internal
+ : zbx_lo_source_t::external;
+ case zbx_lo_t::TX1_LO1:
+ return _regs.TX1_LO_14[idx] == zbx_cpld_regs_t::TX1_LO_14_INTERNAL
+ ? zbx_lo_source_t::internal
+ : zbx_lo_source_t::external;
+ case zbx_lo_t::TX1_LO2:
+ return _regs.TX1_LO_13[idx] == zbx_cpld_regs_t::TX1_LO_13_INTERNAL
+ ? zbx_lo_source_t::internal
+ : zbx_lo_source_t::external;
+ case zbx_lo_t::RX0_LO1:
+ return _regs.RX0_LO_9[idx] == zbx_cpld_regs_t::RX0_LO_9_INTERNAL
+ ? zbx_lo_source_t::internal
+ : zbx_lo_source_t::external;
+ case zbx_lo_t::RX0_LO2:
+ return _regs.RX0_LO_10[idx] == zbx_cpld_regs_t::RX0_LO_10_INTERNAL
+ ? zbx_lo_source_t::internal
+ : zbx_lo_source_t::external;
+ case zbx_lo_t::RX1_LO1:
+ return _regs.RX1_LO_9[idx] == zbx_cpld_regs_t::RX1_LO_9_INTERNAL
+ ? zbx_lo_source_t::internal
+ : zbx_lo_source_t::external;
+ case zbx_lo_t::RX1_LO2:
+ return _regs.RX1_LO_10[idx] == zbx_cpld_regs_t::RX1_LO_10_INTERNAL
+ ? zbx_lo_source_t::internal
+ : zbx_lo_source_t::external;
+ default:
+ UHD_THROW_INVALID_CODE_PATH();
+ }
+}
+
+void zbx_cpld_ctrl::pulse_lo_sync(const size_t ref_chan, const std::vector<zbx_lo_t>& los)
+{
+ if (_regs.BYPASS_SYNC_REGISTER == zbx_cpld_regs_t::BYPASS_SYNC_REGISTER_ENABLE) {
+ const std::string err_msg = "Cannot pulse LO SYNC when bypass is enabled!";
+ UHD_LOG_ERROR(_log_id, err_msg);
+ throw uhd::runtime_error(_log_id + err_msg);
+ }
+ // Assert a 1 for all LOs to be sync'd
+ static const std::unordered_map<zbx_lo_t, zbx_cpld_regs_t::zbx_cpld_field_t>
+ lo_pulse_map{{
+ {zbx_lo_t::TX0_LO1, zbx_cpld_regs_t::zbx_cpld_field_t::PULSE_TX0_LO1_SYNC},
+ {zbx_lo_t::TX0_LO2, zbx_cpld_regs_t::zbx_cpld_field_t::PULSE_TX0_LO2_SYNC},
+ {zbx_lo_t::TX1_LO1, zbx_cpld_regs_t::zbx_cpld_field_t::PULSE_TX1_LO1_SYNC},
+ {zbx_lo_t::TX1_LO2, zbx_cpld_regs_t::zbx_cpld_field_t::PULSE_TX1_LO2_SYNC},
+ {zbx_lo_t::RX0_LO1, zbx_cpld_regs_t::zbx_cpld_field_t::PULSE_RX0_LO1_SYNC},
+ {zbx_lo_t::RX0_LO2, zbx_cpld_regs_t::zbx_cpld_field_t::PULSE_RX0_LO2_SYNC},
+ {zbx_lo_t::RX1_LO1, zbx_cpld_regs_t::zbx_cpld_field_t::PULSE_RX1_LO1_SYNC},
+ {zbx_lo_t::RX1_LO2, zbx_cpld_regs_t::zbx_cpld_field_t::PULSE_RX1_LO2_SYNC},
+ }};
+ for (const auto lo : los) {
+ _regs.set_field(lo_pulse_map.at(lo), 1);
+ }
+ commit(ref_chan == 0 ? CHAN0 : CHAN1);
+ // The bits are strobed, they self-clear. We reflect that here by resetting
+ // them without another commit:
+ for (const auto lo_it : lo_pulse_map) {
+ _regs.set_field(lo_it.second, 0);
+ }
+ _regs.save_state();
+}
+
+void zbx_cpld_ctrl::set_lo_sync_bypass(const bool enable)
+{
+ _regs.BYPASS_SYNC_REGISTER = enable ? zbx_cpld_regs_t::BYPASS_SYNC_REGISTER_ENABLE
+ : zbx_cpld_regs_t::BYPASS_SYNC_REGISTER_DISABLE;
+ commit(NO_CHAN);
+}
+
+void zbx_cpld_ctrl::update_tx_dsa_settings(
+ const std::vector<uint32_t>& dsa1_table, const std::vector<uint32_t>& dsa2_table)
+{
+ write_register_vector("TX0_TABLE_DSA1", dsa1_table);
+ write_register_vector("TX0_TABLE_DSA2", dsa2_table);
+ write_register_vector("TX1_TABLE_DSA1", dsa1_table);
+ write_register_vector("TX1_TABLE_DSA2", dsa2_table);
+ commit(NO_CHAN);
+}
+
+void zbx_cpld_ctrl::update_rx_dsa_settings(const std::vector<uint32_t>& dsa1_table,
+ const std::vector<uint32_t>& dsa2_table,
+ const std::vector<uint32_t>& dsa3a_table,
+ const std::vector<uint32_t>& dsa3b_table)
+{
+ write_register_vector("RX0_TABLE_DSA1", dsa1_table);
+ write_register_vector("RX0_TABLE_DSA2", dsa2_table);
+ write_register_vector("RX0_TABLE_DSA3_A", dsa3a_table);
+ write_register_vector("RX0_TABLE_DSA3_B", dsa3b_table);
+ write_register_vector("RX1_TABLE_DSA1", dsa1_table);
+ write_register_vector("RX1_TABLE_DSA2", dsa2_table);
+ write_register_vector("RX1_TABLE_DSA3_A", dsa3a_table);
+ write_register_vector("RX1_TABLE_DSA3_B", dsa3b_table);
+ commit(NO_CHAN);
+}
+
+/******************************************************************************
+ * Private methods
+ *****************************************************************************/
+void zbx_cpld_ctrl::_lo_spi_transact(const zbx_lo_t lo,
+ const uint8_t addr,
+ const uint16_t data,
+ const spi_xact_t xact_type,
+ const bool throttle)
+{
+ // Look up the channel based on the LO, so we can load the correct command
+ // time for the poke
+ const chan_t chan = (lo == zbx_lo_t::TX0_LO1 || lo == zbx_lo_t::TX0_LO2
+ || lo == zbx_lo_t::RX0_LO1 || lo == zbx_lo_t::RX0_LO2)
+ ? CHAN0
+ : CHAN1;
+ // Note: For SPI transactions, we can't also be lugging around other
+ // registers. This means that we assume that the state of _regs is clean.
+ _regs.ADDRESS = addr;
+ _regs.DATA = data;
+ _regs.READ_FLAG = (xact_type == spi_xact_t::WRITE) ? zbx_cpld_regs_t::READ_FLAG_WRITE
+ : zbx_cpld_regs_t::READ_FLAG_READ;
+ _regs.LO_SELECT = zbx_cpld_regs_t::LO_SELECT_t(lo);
+ _regs.START_TRANSACTION = zbx_cpld_regs_t::START_TRANSACTION_ENABLE;
+ _poke32(_lo_spi_offset, _regs.get_reg(_lo_spi_offset), chan);
+ _regs.START_TRANSACTION = zbx_cpld_regs_t::START_TRANSACTION_DISABLE;
+ _regs.save_state();
+ // Write complete. Now we need to send a sleep to throttle the SPI
+ // transactions:
+ if (throttle) {
+ _sleep(SPI_THROTTLE_TIME);
+ }
+}
+
+void zbx_cpld_ctrl::write_register_vector(
+ const std::string& reg_addr_name, const std::vector<uint32_t>& values)
+{
+ UHD_LOG_DEBUG(
+ _log_id, "Write " << values.size() << " values to register " << reg_addr_name);
+ zbx_cpld_regs_t::zbx_cpld_field_t type = _regs.get_field_type(reg_addr_name);
+ if (values.size() > _regs.get_array_size(type)) {
+ const std::string err_msg = "Number of values passed for register vector("
+ + std::to_string(values.size())
+ + ") exceeds size of register ("
+ + std::to_string(_regs.get_array_size(type)) + ")";
+ UHD_LOG_ERROR(_log_id, err_msg);
+ throw uhd::runtime_error(err_msg);
+ }
+ for (size_t i = 0; i < values.size(); i++) {
+ _regs.set_field(type, values[i], i);
+ }
+}
+
+void zbx_cpld_ctrl::commit(const chan_t chan, const bool save_all)
+{
+ UHD_LOG_TRACE(_log_id,
+ "Storing register cache " << (save_all ? "completely" : "selectively")
+ << " to CPLD...");
+ const auto changed_addrs = save_all ? _regs.get_all_addrs()
+ : _regs.get_changed_addrs<size_t>();
+ for (const auto addr : changed_addrs) {
+ _poke32(addr, _regs.get_reg(addr), save_all ? NO_CHAN : chan);
+ }
+ _regs.save_state();
+ UHD_LOG_TRACE(_log_id,
+ "Storing cache complete: "
+ "Updated "
+ << changed_addrs.size() << " registers.");
+}
+
+void zbx_cpld_ctrl::update_field(
+ const zbx_cpld_regs_t::zbx_cpld_field_t field, const size_t idx)
+{
+ const uint16_t addr = _regs.get_addr(field) + 4 * idx;
+ const uint32_t chip_val = _peek32(addr);
+ _regs.set_reg(addr, chip_val);
+ const auto changed_addrs = _regs.get_changed_addrs<size_t>();
+ // If this is the only change in our register stack, then we call save_state()
+ // because we don't want to write this value we just read from the CPLD back
+ // to it. However, if there are other changes queued up, we'll have to wait
+ // until the next commit() call. If this is not desired, we need to update
+ // the regmap code to selectively save state.
+ if (changed_addrs.empty()
+ || (changed_addrs.size() == 1 && changed_addrs.count(addr))) {
+ _regs.save_state();
+ } else {
+ UHD_LOG_DEBUG(_log_id,
+ "Not saving register state after calling update_field(). This may "
+ "cause unnecessary writes in the future.");
+ }
+}
+
+}}} // namespace uhd::usrp::zbx
diff --git a/host/lib/usrp/dboard/zbx/zbx_dboard.cpp b/host/lib/usrp/dboard/zbx/zbx_dboard.cpp
new file mode 100644
index 000000000..d41302c8f
--- /dev/null
+++ b/host/lib/usrp/dboard/zbx/zbx_dboard.cpp
@@ -0,0 +1,758 @@
+//
+// Copyright 2020 Ettus Research, a National Instruments Brand
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+#include <uhd/types/direction.hpp>
+#include <uhd/types/eeprom.hpp>
+#include <uhd/utils/algorithm.hpp>
+#include <uhd/utils/assert_has.hpp>
+#include <uhd/utils/log.hpp>
+#include <uhd/utils/math.hpp>
+#include <uhdlib/usrp/dboard/zbx/zbx_dboard.hpp>
+#include <uhdlib/utils/narrow.hpp>
+#include <cstdlib>
+#include <sstream>
+
+namespace uhd { namespace usrp { namespace zbx {
+
+/******************************************************************************
+ * Structors
+ *****************************************************************************/
+zbx_dboard_impl::zbx_dboard_impl(register_iface& reg_iface,
+ const size_t reg_base_address,
+ time_accessor_fn_type&& time_accessor,
+ const size_t db_idx,
+ const std::string& radio_slot,
+ const std::string& rpc_prefix,
+ const std::string& unique_id,
+ uhd::usrp::x400_rpc_iface::sptr mb_rpcc,
+ uhd::usrp::zbx_rpc_iface::sptr rpcc,
+ uhd::rfnoc::x400::rfdc_control::sptr rfdcc,
+ uhd::property_tree::sptr tree)
+ : _unique_id(unique_id)
+ , _regs(reg_iface)
+ , _reg_base_address(reg_base_address)
+ , _time_accessor(time_accessor)
+ , _radio_slot(radio_slot)
+ , _db_idx(db_idx)
+ , _rpc_prefix(rpc_prefix)
+ , _mb_rpcc(mb_rpcc)
+ , _rpcc(rpcc)
+ , _rfdcc(rfdcc)
+ , _tree(tree)
+ , _rfdc_rate(_rpcc->get_dboard_sample_rate())
+ , _prc_rate(_rpcc->get_dboard_prc_rate())
+{
+ RFNOC_LOG_TRACE("Entering zbx_dboard_impl ctor...");
+ RFNOC_LOG_TRACE("Radio slot: " << _radio_slot);
+
+ _tx_gain_profile_api = std::make_shared<rf_control::enumerated_gain_profile>(
+ ZBX_GAIN_PROFILES, ZBX_GAIN_PROFILE_DEFAULT, ZBX_NUM_CHANS);
+ _rx_gain_profile_api = std::make_shared<rf_control::enumerated_gain_profile>(
+ ZBX_GAIN_PROFILES, ZBX_GAIN_PROFILE_DEFAULT, ZBX_NUM_CHANS);
+
+ _expert_container =
+ uhd::experts::expert_factory::create_container("zbx_radio_" + _radio_slot);
+ _init_cpld();
+ _init_peripherals();
+ // Prop tree requires the initialization of certain peripherals
+ _init_prop_tree();
+ _expert_container->resolve_all();
+}
+
+zbx_dboard_impl::~zbx_dboard_impl()
+{
+ RFNOC_LOG_TRACE("zbx_dboard::dtor() ");
+}
+
+void zbx_dboard_impl::deinit()
+{
+ _wb_ifaces.clear();
+}
+
+void zbx_dboard_impl::set_command_time(uhd::time_spec_t time, const size_t chan)
+{
+ // When the command time gets updated, import it into the expert graph
+ get_tree()
+ ->access<time_spec_t>(fs_path("dboard") / "rx_frontends" / chan / "time/cmd")
+ .set(time);
+}
+
+std::string zbx_dboard_impl::get_unique_id() const
+{
+ return _unique_id;
+}
+
+
+/******************************************************************************
+ * API Calls
+ *****************************************************************************/
+void zbx_dboard_impl::set_tx_antenna(const std::string& ant, const size_t chan)
+{
+ RFNOC_LOG_TRACE("Setting TX antenna to " << ant << " for chan " << chan);
+ if (!TX_ANTENNA_NAME_COMPAT_MAP.count(ant)) {
+ assert_has(TX_ANTENNAS, ant, "tx antenna");
+ }
+ const fs_path fe_path = _get_frontend_path(TX_DIRECTION, chan);
+
+ _tree->access<std::string>(fe_path / "antenna" / "value").set(ant);
+}
+
+void zbx_dboard_impl::set_rx_antenna(const std::string& ant, const size_t chan)
+{
+ RFNOC_LOG_TRACE("Setting RX antenna to " << ant << " for chan " << chan);
+ if (!RX_ANTENNA_NAME_COMPAT_MAP.count(ant)) {
+ assert_has(RX_ANTENNAS, ant, "rx antenna");
+ }
+
+ const fs_path fe_path = _get_frontend_path(RX_DIRECTION, chan);
+
+ _tree->access<std::string>(fe_path / "antenna" / "value").set(ant);
+}
+
+double zbx_dboard_impl::set_tx_frequency(const double req_freq, const size_t chan)
+{
+ const fs_path fe_path = _get_frontend_path(TX_DIRECTION, chan);
+
+ _tree->access<double>(fe_path / "freq").set(req_freq);
+
+ // Our power manager sets a new gain value via the API, based on its new calculations.
+ // Since the expert nodes are protected by a mutex, it will hang if we try to call
+ // update_power() from inside the expert resolve methods (resolve() -> update_power()
+ // -> set_tx_gain -> resolve())
+ _tx_pwr_mgr.at(chan)->update_power();
+
+ return _tree->access<double>(fe_path / "freq").get();
+}
+
+double zbx_dboard_impl::set_rx_frequency(const double req_freq, const size_t chan)
+{
+ const fs_path fe_path = _get_frontend_path(RX_DIRECTION, chan);
+
+ _tree->access<double>(fe_path / "freq").set(req_freq);
+
+ // Our power manager sets a new gain value via the API, based on its new calculations.
+ // Since the expert nodes are protected by a mutex, it will hang if we try to call
+ // update_power() from inside the expert resolve methods (resolve() -> update_power()
+ // -> set_rx_gain -> resolve())
+ _rx_pwr_mgr.at(chan)->update_power();
+
+ return _tree->access<double>(fe_path / "freq").get();
+}
+
+double zbx_dboard_impl::set_tx_bandwidth(const double bandwidth, const size_t chan)
+{
+ const double bw = get_tx_bandwidth(chan);
+ if (!uhd::math::frequencies_are_equal(bandwidth, bw)) {
+ RFNOC_LOG_WARNING("Invalid analog bandwidth: " << (bandwidth / 1e6) << " MHz.");
+ }
+ return bw;
+}
+
+double zbx_dboard_impl::get_tx_bandwidth(size_t chan)
+{
+ return _tree
+ ->access<double>(_get_frontend_path(TX_DIRECTION, chan) / "bandwidth/value")
+ .get();
+}
+
+double zbx_dboard_impl::set_rx_bandwidth(const double bandwidth, const size_t chan)
+{
+ const double bw = get_rx_bandwidth(chan);
+ if (!uhd::math::frequencies_are_equal(bandwidth, bw)) {
+ RFNOC_LOG_WARNING("Invalid analog bandwidth: " << (bandwidth / 1e6) << " MHz.");
+ }
+ return bw;
+}
+
+double zbx_dboard_impl::get_rx_bandwidth(size_t chan)
+{
+ return _tree
+ ->access<double>(_get_frontend_path(RX_DIRECTION, chan) / "bandwidth/value")
+ .get();
+}
+
+double zbx_dboard_impl::set_tx_gain(
+ const double gain, const std::string& name_, const size_t chan)
+{
+ // We have to accept the empty string for "all", because that's widely used
+ // (e.g. by multi_usrp)
+ const std::string name = name_.empty() ? ZBX_GAIN_STAGE_ALL : name_;
+ const fs_path gains_path = _get_frontend_path(TX_DIRECTION, chan) / "gains";
+ const auto gain_profile = _tx_gain_profile_api->get_gain_profile(chan);
+ // Default gain profile: Setting anything other than 'all' is forbidden
+ if (gain_profile == ZBX_GAIN_PROFILE_DEFAULT && name != ZBX_GAIN_STAGE_ALL) {
+ throw uhd::key_error("Invalid gain name for gain profile 'default': " + name);
+ }
+ // Also, when the gain name is all, we have to be in default mode.
+ if (gain_profile != ZBX_GAIN_PROFILE_DEFAULT && name == ZBX_GAIN_STAGE_ALL) {
+ throw uhd::key_error(
+ "Setting overall gain is only valid in gain profile 'default'!");
+ }
+ // The combination of the no-ATR profile, and any gain name other than 'table'
+ // is not valid.
+ if (gain_profile == ZBX_GAIN_PROFILE_CPLD_NOATR && name != ZBX_GAIN_STAGE_TABLE) {
+ throw uhd::key_error("set_tx_gain(): Invalid combination of gain profile "
+ + gain_profile + " and gain name " + name);
+ }
+ // First, we handle the 'table' gain name. It's handled a bit differently
+ // than the rest.
+ if (name == ZBX_GAIN_STAGE_TABLE) {
+ static const uhd::meta_range_t table_range(0, 255, 1);
+ const uint8_t table_idx = uhd::narrow<uint8_t>(table_range.clip(gain, true));
+ if (gain_profile == ZBX_GAIN_PROFILE_CPLD_NOATR) {
+ _cpld->set_sw_config(chan, zbx_cpld_ctrl::atr_mode_target::DSA, table_idx);
+ return static_cast<double>(table_idx);
+ }
+ if (gain_profile == ZBX_GAIN_PROFILE_MANUAL
+ || gain_profile == ZBX_GAIN_PROFILE_CPLD) {
+ _cpld->set_tx_gain_switches(chan, ATR_ADDR_TX, table_idx);
+ _cpld->set_tx_gain_switches(chan, ATR_ADDR_XX, table_idx);
+ return static_cast<double>(table_idx);
+ }
+ // That covers all the gain profiles for gain name 'table'.
+ UHD_THROW_INVALID_CODE_PATH();
+ }
+ // Sanity check key. Note we do this after the previous gain stage, because
+ // it's not a property node.
+ if (!_tree->exists(gains_path / name)) {
+ throw uhd::key_error("Invalid TX gain stage: " + name);
+ }
+ // This leaves directly setting either the DSAs or the amplifier. This is
+ // possible in both the manual and CPLD gain profiles.
+ return _tree->access<double>(gains_path / name / "value").set(gain).get();
+}
+
+double zbx_dboard_impl::set_rx_gain(
+ const double gain, const std::string& name_, const size_t chan)
+{
+ // We have to accept the empty string for "all", because that's widely used
+ // (e.g. by multi_usrp).
+ const std::string name = name_.empty() ? ZBX_GAIN_STAGE_ALL : name_;
+ const fs_path gains_path = _get_frontend_path(RX_DIRECTION, chan) / "gains";
+ const auto gain_profile = _rx_gain_profile_api->get_gain_profile(chan);
+
+ // Default gain profile: Setting anything other than ZBX_GAIN_STAGE_ALL is forbidden
+ if (gain_profile == ZBX_GAIN_PROFILE_DEFAULT && name != ZBX_GAIN_STAGE_ALL) {
+ throw uhd::key_error("Invalid gain name for gain profile 'default': " + name);
+ }
+ // Also, when the gain name is all, we have to be in default mode.
+ if (gain_profile != ZBX_GAIN_PROFILE_DEFAULT && name == ZBX_GAIN_STAGE_ALL) {
+ throw uhd::key_error(
+ "Setting overall gain is only valid in gain profile 'default'!");
+ }
+ // The combination of the no-ATR profile, and any gain name other than 'table'
+ // is not valid.
+ if (gain_profile == ZBX_GAIN_PROFILE_CPLD_NOATR && name != ZBX_GAIN_STAGE_TABLE) {
+ throw uhd::key_error("set_rx_gain(): Invalid combination of gain profile "
+ + gain_profile + " and gain name " + name);
+ }
+ // First, we handle the 'table' gain name. It's a bit different from the
+ // rest.
+ if (name == ZBX_GAIN_STAGE_TABLE) {
+ static const uhd::meta_range_t table_range(0, 255, 1);
+ const uint8_t table_idx = uhd::narrow<uint8_t>(table_range.clip(gain, true));
+ if (gain_profile == ZBX_GAIN_PROFILE_CPLD_NOATR) {
+ _cpld->set_sw_config(chan, zbx_cpld_ctrl::atr_mode_target::DSA, table_idx);
+ return static_cast<double>(table_idx);
+ }
+ if (gain_profile == ZBX_GAIN_PROFILE_MANUAL
+ || gain_profile == ZBX_GAIN_PROFILE_CPLD) {
+ _cpld->set_rx_gain_switches(chan, ATR_ADDR_RX, table_idx);
+ _cpld->set_rx_gain_switches(chan, ATR_ADDR_XX, table_idx);
+ return static_cast<double>(table_idx);
+ }
+ // That covers all the gain profiles for gain name 'table'.
+ UHD_THROW_INVALID_CODE_PATH();
+ }
+ // Sanity check key. Note we do this after the previous gain stage, because
+ // it's not a property node.
+ if (!_tree->exists(gains_path / name / "value")) {
+ throw uhd::key_error("Invalid RX gain stage: " + name);
+ }
+ return _tree->access<double>(gains_path / name / "value").set(gain).get();
+}
+
+double zbx_dboard_impl::set_tx_gain(const double gain, const size_t chan)
+{
+ const auto gain_profile = _tx_gain_profile_api->get_gain_profile(chan);
+ if (gain_profile == ZBX_GAIN_PROFILE_MANUAL) {
+ const std::string err_msg = "When using 'manual' gain mode, calling "
+ "set_tx_gain() without a gain name is not allowed!";
+ RFNOC_LOG_ERROR(err_msg);
+ throw uhd::runtime_error(err_msg);
+ }
+ if (gain_profile == ZBX_GAIN_PROFILE_CPLD
+ || gain_profile == ZBX_GAIN_PROFILE_CPLD_NOATR) {
+ return set_tx_gain(gain, ZBX_GAIN_STAGE_TABLE, chan);
+ }
+ return set_tx_gain(gain, ZBX_GAIN_STAGE_ALL, chan);
+}
+
+double zbx_dboard_impl::set_rx_gain(const double gain, const size_t chan)
+{
+ const auto gain_profile = _rx_gain_profile_api->get_gain_profile(chan);
+ if (gain_profile == ZBX_GAIN_PROFILE_MANUAL) {
+ const std::string err_msg = "When using 'manual' gain mode, calling "
+ "set_rx_gain() without a gain name is not allowed!";
+ RFNOC_LOG_ERROR(err_msg);
+ throw uhd::runtime_error(err_msg);
+ }
+ if (gain_profile == ZBX_GAIN_PROFILE_CPLD
+ || gain_profile == ZBX_GAIN_PROFILE_CPLD_NOATR) {
+ return set_rx_gain(gain, ZBX_GAIN_STAGE_TABLE, chan);
+ }
+ return set_rx_gain(gain, ZBX_GAIN_STAGE_ALL, chan);
+}
+
+double zbx_dboard_impl::get_tx_gain(const size_t chan)
+{
+ const auto gain_profile = _tx_gain_profile_api->get_gain_profile(chan);
+ if (gain_profile == ZBX_GAIN_PROFILE_CPLD
+ || gain_profile == ZBX_GAIN_PROFILE_CPLD_NOATR) {
+ return get_tx_gain(ZBX_GAIN_STAGE_TABLE, chan);
+ }
+ if (gain_profile == ZBX_GAIN_PROFILE_DEFAULT) {
+ return get_tx_gain(ZBX_GAIN_STAGE_ALL, chan);
+ }
+ throw uhd::runtime_error(
+ "get_tx_gain(): When in 'manual' gain profile, a gain name is required!");
+}
+
+double zbx_dboard_impl::get_rx_gain(const size_t chan)
+{
+ const auto gain_profile = _rx_gain_profile_api->get_gain_profile(chan);
+ if (gain_profile == ZBX_GAIN_PROFILE_CPLD
+ || gain_profile == ZBX_GAIN_PROFILE_CPLD_NOATR) {
+ return get_rx_gain(ZBX_GAIN_STAGE_TABLE, chan);
+ }
+ if (gain_profile == ZBX_GAIN_PROFILE_DEFAULT) {
+ return get_rx_gain(ZBX_GAIN_STAGE_ALL, chan);
+ }
+ throw uhd::runtime_error(
+ "get_rx_gain(): When in 'manual' gain profile, a gain name is required!");
+}
+
+double zbx_dboard_impl::get_tx_gain(const std::string& name_, const size_t chan)
+{
+ // We have to accept the empty string for "all", because that's widely used
+ // (e.g. by multi_usrp)
+ const std::string name = name_.empty() ? ZBX_GAIN_STAGE_ALL : name_;
+ const fs_path gains_path = _get_frontend_path(TX_DIRECTION, chan) / "gains";
+ const auto gain_profile = _tx_gain_profile_api->get_gain_profile(chan);
+ // Overall gain: Only reliable in 'default' mode. We warn, not throw, in
+ // the other modes. That's because reading back the overall gain is common
+ // diagnostic for many existing applications.
+ if (name == ZBX_GAIN_STAGE_ALL && gain_profile != ZBX_GAIN_PROFILE_DEFAULT) {
+ RFNOC_LOG_WARNING("get_tx_gain(): Trying to read back overall gain in "
+ "non-default gain profile is undefined.");
+ }
+ // Table gain: Returns the current DSA table index.
+ if (name == ZBX_GAIN_STAGE_TABLE) {
+ return static_cast<double>(
+ _cpld->get_current_config(chan, zbx_cpld_ctrl::atr_mode_target::DSA));
+ }
+ // Otherwise: DSA or amp. Sanity check key is valid. Because the table gain
+ // is not a property tree node, this check comes after the previous if-clause.
+ if (!_tree->exists(gains_path / name / "value")) {
+ RFNOC_LOG_ERROR("get_tx_gain(): Invalid gain name `" << name << "'");
+ throw uhd::key_error(std::string("get_tx_gain(): Invalid gain name: ") + name);
+ }
+ // We're not yet done: If we're in CPLD/table profiles, we peek the current
+ // DSA settings and apply them to the local cache.
+ // Note: This means we have a different behaviour between directly accessing
+ // the prop tree, or accessing the C++ API.
+ if ((name == ZBX_GAIN_STAGE_DSA1 || name == ZBX_GAIN_STAGE_DSA2)
+ && (gain_profile == ZBX_GAIN_PROFILE_CPLD
+ || gain_profile == ZBX_GAIN_PROFILE_CPLD_NOATR)) {
+ const uint8_t idx =
+ (gain_profile == ZBX_GAIN_PROFILE_CPLD_NOATR)
+ ? _cpld->get_current_config(chan, zbx_cpld_ctrl::atr_mode_target::DSA)
+ : ATR_ADDR_TX;
+ constexpr bool update_cache = true; // Make sure to peek the actual value
+ const auto dsa = (name == ZBX_GAIN_STAGE_DSA1) ? zbx_cpld_ctrl::dsa_type::DSA1
+ : zbx_cpld_ctrl::dsa_type::DSA2;
+ const uint8_t dsa_val = _cpld->get_tx_dsa(chan, idx, dsa, update_cache);
+ // Update the tree because we're good citizens, and if we switch the
+ // gain profile from 'table' to 'manual', we want everything to be
+ // consistent. This will not cause a poke to the CPLD, b/c the experts
+ // won't write gains in this gain profile.
+ // Note that the other DSA values in the tree are not updated automatically,
+ // which is why we can't write DSA values to the CPLD in this mode. If
+ // we want to allow writing DSA values in this mode, we need to update
+ // everything here, or put some more cleverness into the programming
+ // expert.
+ _tree->access<double>(gains_path / name / "value")
+ .set(ZBX_TX_DSA_MAX_ATT - dsa_val);
+ }
+ // Now return the value from the tree
+ return _tree->access<double>(gains_path / name / "value").get();
+}
+
+double zbx_dboard_impl::get_rx_gain(const std::string& name_, const size_t chan)
+{
+ // We have to accept the empty string for "all", because that's widely used
+ // (e.g. by multi_usrp)
+ const std::string name = name_.empty() ? ZBX_GAIN_STAGE_ALL : name_;
+ const fs_path gains_path = _get_frontend_path(RX_DIRECTION, chan) / "gains";
+ const auto gain_profile = _rx_gain_profile_api->get_gain_profile(chan);
+ // Overall gain: Only reliable in 'default' mode. We warn, not throw, in
+ // the other modes. That's because reading back the overall gain is common
+ // diagnostic for many existing applications.
+ if (name == ZBX_GAIN_STAGE_ALL && gain_profile != ZBX_GAIN_PROFILE_DEFAULT) {
+ RFNOC_LOG_WARNING("get_rx_gain(): Trying to read back overall gain in "
+ "non-default gain profile is undefined.");
+ }
+ // Table gain: Returns the current DSA table index.
+ if (name == ZBX_GAIN_STAGE_TABLE) {
+ return static_cast<double>(
+ _cpld->get_current_config(chan, zbx_cpld_ctrl::atr_mode_target::DSA));
+ }
+ // Otherwise: DSA. Sanity check key is valid. Because the table gain is not
+ // a property tree node, this check comes after the previous if-clause.
+ if (!_tree->exists(gains_path / name / "value")) {
+ RFNOC_LOG_ERROR("get_rx_gain(): Invalid gain name `" << name << "'");
+ throw uhd::key_error(std::string("get_rx_gain(): Invalid gain name: ") + name);
+ }
+ // We're not yet done: If we're in CPLD/table profiles, we peek the current
+ // DSA settings and apply them to the local cache.
+ // Note: This means we have a different behaviour between directly accessing
+ // the prop tree, or accessing the C++ API.
+ if (gain_profile == ZBX_GAIN_PROFILE_CPLD
+ || gain_profile == ZBX_GAIN_PROFILE_CPLD_NOATR) {
+ const uint8_t idx =
+ (gain_profile == ZBX_GAIN_PROFILE_CPLD_NOATR)
+ ? _cpld->get_current_config(chan, zbx_cpld_ctrl::atr_mode_target::DSA)
+ : ATR_ADDR_RX;
+ constexpr bool update_cache = true; // Make sure to peek the actual value
+ static const std::map<std::string, zbx_cpld_ctrl::dsa_type> dsa_map{
+ {ZBX_GAIN_STAGE_DSA1, zbx_cpld_ctrl::dsa_type::DSA1},
+ {ZBX_GAIN_STAGE_DSA2, zbx_cpld_ctrl::dsa_type::DSA2},
+ {ZBX_GAIN_STAGE_DSA3A, zbx_cpld_ctrl::dsa_type::DSA3A},
+ {ZBX_GAIN_STAGE_DSA3B, zbx_cpld_ctrl::dsa_type::DSA3B},
+ };
+ const auto dsa = dsa_map.at(name);
+ const uint8_t dsa_val = _cpld->get_rx_dsa(chan, idx, dsa, update_cache);
+ // Update the tree because we're good citizens, and if we switch the
+ // gain profile from 'table' to 'manual', we want everything to be
+ // consistent. This will not cause a poke to the CPLD, b/c the experts
+ // won't write gains in this gain profile.
+ // Note that the other DSA values in the tree are not updated automatically,
+ // which is why we can't write DSA values to the CPLD in this profile. If
+ // we want to allow writing DSA values in this profile, we need to update
+ // everything here, or put some more cleverness into the programming
+ // expert.
+ _tree->access<double>(gains_path / name / "value")
+ .set(static_cast<double>(ZBX_RX_DSA_MAX_ATT - dsa_val));
+ }
+ return _tree->access<double>(gains_path / name / "value").get();
+}
+
+std::vector<std::string> zbx_dboard_impl::get_tx_gain_names(const size_t chan) const
+{
+ UHD_ASSERT_THROW(chan < ZBX_NUM_CHANS);
+ const std::string gain_profile = _tx_gain_profile_api->get_gain_profile(chan);
+
+ if (gain_profile == ZBX_GAIN_PROFILE_DEFAULT) {
+ return {ZBX_GAIN_STAGE_ALL};
+ }
+ if (gain_profile == ZBX_GAIN_PROFILE_CPLD
+ || gain_profile == ZBX_GAIN_PROFILE_CPLD_NOATR) {
+ return {ZBX_GAIN_STAGE_TABLE};
+ }
+ return ZBX_TX_GAIN_STAGES;
+}
+
+std::vector<std::string> zbx_dboard_impl::get_rx_gain_names(const size_t chan) const
+{
+ UHD_ASSERT_THROW(chan < ZBX_NUM_CHANS);
+ const std::string gain_profile = _rx_gain_profile_api->get_gain_profile(chan);
+
+ if (gain_profile == ZBX_GAIN_PROFILE_DEFAULT) {
+ return {ZBX_GAIN_STAGE_ALL};
+ }
+ if (gain_profile == ZBX_GAIN_PROFILE_CPLD
+ || gain_profile == ZBX_GAIN_PROFILE_CPLD_NOATR) {
+ return {ZBX_GAIN_STAGE_TABLE};
+ }
+ return ZBX_RX_GAIN_STAGES;
+}
+
+const std::string zbx_dboard_impl::get_tx_lo_source(
+ const std::string& name, const size_t chan)
+{
+ const fs_path fe_path = _get_frontend_path(TX_DIRECTION, chan);
+ if (!_tree->exists(fe_path / "ch" / name)) {
+ throw uhd::value_error("get_tx_lo_source(): Invalid LO name: " + name);
+ }
+
+ const zbx_lo_source_t lo_source =
+ _tree->access<zbx_lo_source_t>(fe_path / "ch" / name / "source").get();
+ return lo_source == zbx_lo_source_t::internal ? "internal" : "external";
+}
+
+const std::string zbx_dboard_impl::get_rx_lo_source(
+ const std::string& name, const size_t chan)
+{
+ const fs_path fe_path = _get_frontend_path(RX_DIRECTION, chan);
+ if (!_tree->exists(fe_path / "ch" / name)) {
+ throw uhd::value_error("get_rx_lo_source(): Invalid LO name: " + name);
+ }
+
+ const zbx_lo_source_t lo_source =
+ _tree->access<zbx_lo_source_t>(fe_path / "ch" / name / "source").get();
+ return lo_source == zbx_lo_source_t::internal ? "internal" : "external";
+}
+
+void zbx_dboard_impl::set_rx_lo_source(
+ const std::string& src, const std::string& name, const size_t chan)
+{
+ RFNOC_LOG_TRACE("set_rx_lo_source(name=" << name << ", src=" << src << ")");
+ const fs_path fe_path = _get_frontend_path(RX_DIRECTION, chan);
+ if (!_tree->exists(fe_path / "ch" / name)) {
+ throw uhd::value_error("set_rx_lo_source(): Invalid LO name: " + name);
+ }
+
+ _tree->access<zbx_lo_source_t>(fe_path / "ch" / name / "source")
+ .set(src == "internal" ? zbx_lo_source_t::internal : zbx_lo_source_t::external);
+}
+
+void zbx_dboard_impl::set_tx_lo_source(
+ const std::string& src, const std::string& name, const size_t chan)
+{
+ RFNOC_LOG_TRACE("set_tx_lo_source(name=" << name << ", src=" << src << ")");
+ const fs_path fe_path = _get_frontend_path(TX_DIRECTION, chan);
+ if (!_tree->exists(fe_path / "ch" / name)) {
+ throw uhd::value_error("set_tx_lo_source(): Invalid LO name: " + name);
+ }
+
+ _tree->access<zbx_lo_source_t>(fe_path / "ch" / name / "source")
+ .set(src == "internal" ? zbx_lo_source_t::internal : zbx_lo_source_t::external);
+}
+
+double zbx_dboard_impl::set_tx_lo_freq(
+ double freq, const std::string& name, const size_t chan)
+{
+ RFNOC_LOG_TRACE("set_tx_lo_freq(freq=" << freq << ", name=" << name << ")");
+ const fs_path fe_path = _get_frontend_path(TX_DIRECTION, chan);
+ assert_has(ZBX_LOS, name);
+
+ return _tree->access<double>(fe_path / "los" / name / "freq" / "value").set(freq).get();
+}
+
+double zbx_dboard_impl::get_tx_lo_freq(const std::string& name, const size_t chan)
+{
+ RFNOC_LOG_TRACE("get_tx_lo_freq(name=" << name << ")");
+ const fs_path fe_path = _get_frontend_path(TX_DIRECTION, chan);
+ assert_has(ZBX_LOS, name);
+
+ return _tree->access<double>(fe_path / "los" / name / "freq" / "value").get();
+}
+
+freq_range_t zbx_dboard_impl::_get_lo_freq_range(
+ const std::string& name, const size_t /*chan*/) const
+{
+ if (name == ZBX_LO1 || name == ZBX_LO2) {
+ // Note this doesn't include the LO step size. The LO step size is only
+ // used when the LO frequencies are automatically calculated (which is
+ // the normal use case). When setting LO frequencies manually, it is
+ // possible to set LOs to values outside of the step size.
+ return freq_range_t{LMX2572_MIN_FREQ, LMX2572_MAX_FREQ};
+ }
+ if (name == RFDC_NCO) {
+ // It might make sense to constrain the possible NCO values more, since
+ // the bandpass filters for IF2 only allow a certain range. Note that LO1
+ // and LO2 freq ranges are also constrained by their analog filters.
+ // But in principle, this is the range for the NCO... so why not.
+ return freq_range_t{0.0, _rfdc_rate};
+ }
+ throw uhd::value_error("Invalid LO name: " + name);
+}
+
+double zbx_dboard_impl::set_rx_lo_freq(
+ double freq, const std::string& name, const size_t chan)
+{
+ RFNOC_LOG_TRACE("set_rx_lo_freq(freq=" << freq << ", name=" << name << ")");
+ const fs_path fe_path = _get_frontend_path(RX_DIRECTION, chan);
+ assert_has(ZBX_LOS, name);
+
+ return _tree->access<double>(fe_path / "los" / name / "freq" / "value")
+ .set(freq)
+ .get();
+}
+
+double zbx_dboard_impl::get_rx_lo_freq(const std::string& name, size_t chan)
+{
+ RFNOC_LOG_TRACE("get_rx_lo_freq(name=" << name << ")");
+ const fs_path fe_path = _get_frontend_path(RX_DIRECTION, chan);
+ assert_has(ZBX_LOS, name);
+
+ return _tree->access<double>(fe_path / "los" / name / "freq" / "value").get();
+}
+
+std::string zbx_dboard_impl::get_tx_antenna(size_t chan) const
+{
+ const fs_path fe_path = _get_frontend_path(TX_DIRECTION, chan);
+ return _tree->access<std::string>(fe_path / "antenna" / "value").get();
+}
+
+std::string zbx_dboard_impl::get_rx_antenna(size_t chan) const
+{
+ const fs_path fe_path = _get_frontend_path(RX_DIRECTION, chan);
+ return _tree->access<std::string>(fe_path / "antenna" / "value").get();
+}
+
+double zbx_dboard_impl::get_tx_frequency(size_t chan)
+{
+ const fs_path fe_path = _get_frontend_path(TX_DIRECTION, chan);
+ return _tree->access<double>(fe_path / "freq").get();
+}
+
+double zbx_dboard_impl::get_rx_frequency(size_t chan)
+{
+ const fs_path fe_path = _get_frontend_path(RX_DIRECTION, chan);
+ return _tree->access<double>(fe_path / "freq").get();
+}
+
+void zbx_dboard_impl::set_tx_tune_args(const uhd::device_addr_t&, const size_t)
+{
+ RFNOC_LOG_TRACE("tune_args not supported by this radio.");
+}
+
+void zbx_dboard_impl::set_rx_tune_args(const uhd::device_addr_t&, const size_t)
+{
+ RFNOC_LOG_TRACE("tune_args not supported by this radio.");
+}
+
+void zbx_dboard_impl::set_rx_agc(const bool, const size_t)
+{
+ throw uhd::not_implemented_error("set_rx_agc() is not supported on this radio!");
+}
+
+uhd::gain_range_t zbx_dboard_impl::get_tx_gain_range(
+ const std::string& name, const size_t chan) const
+{
+ // We have to accept the empty string for "all", because that's widely used
+ // (e.g. by multi_usrp)
+ if (!name.empty() && name != ZBX_GAIN_STAGE_ALL) {
+ throw uhd::value_error(
+ std::string("get_tx_gain_range(): Unknown gain name '") + name + "'!");
+ }
+ return get_tx_gain_range(chan);
+}
+
+uhd::gain_range_t zbx_dboard_impl::get_rx_gain_range(
+ const std::string& name, const size_t chan) const
+{
+ // We have to accept the empty string for "all", because that's widely used
+ // (e.g. by multi_usrp)
+ if (!name.empty() && name != ZBX_GAIN_STAGE_ALL) {
+ throw uhd::value_error(
+ std::string("get_rx_gain_range(): Unknown gain name '") + name + "'!");
+ }
+ return get_rx_gain_range(chan);
+}
+
+void zbx_dboard_impl::set_rx_lo_export_enabled(bool, const std::string&, const size_t)
+{
+ throw uhd::not_implemented_error(
+ "set_rx_lo_export_enabled is not supported on this radio");
+}
+
+bool zbx_dboard_impl::get_rx_lo_export_enabled(const std::string&, const size_t)
+{
+ return false;
+}
+
+void zbx_dboard_impl::set_tx_lo_export_enabled(bool, const std::string&, const size_t)
+{
+ throw uhd::not_implemented_error(
+ "set_rx_lo_export_enabled is not supported on this radio");
+}
+
+bool zbx_dboard_impl::get_tx_lo_export_enabled(const std::string&, const size_t)
+{
+ return false;
+}
+
+/******************************************************************************
+ * EEPROM API
+ *****************************************************************************/
+eeprom_map_t zbx_dboard_impl::get_db_eeprom()
+{
+ return _mb_rpcc->get_db_eeprom(_db_idx);
+}
+
+size_t zbx_dboard_impl::get_chan_from_dboard_fe(
+ const std::string& fe, const uhd::direction_t) const
+{
+ if (fe == "0") {
+ return 0;
+ }
+ if (fe == "1") {
+ return 1;
+ }
+ throw uhd::key_error(std::string("[X400] Invalid frontend: ") + fe);
+}
+
+std::string zbx_dboard_impl::get_dboard_fe_from_chan(
+ const size_t chan, const uhd::direction_t) const
+{
+ if (chan == 0) {
+ return "0";
+ }
+ if (chan == 1) {
+ return "1";
+ }
+ throw uhd::lookup_error(
+ std::string("[X400] Invalid channel: ") + std::to_string(chan));
+}
+
+/*********************************************************************
+ * Private misc/calculative helper functions
+ **********************************************************************/
+
+bool zbx_dboard_impl::_get_all_los_locked(const direction_t dir, const size_t chan)
+{
+ const fs_path fe_path = _get_frontend_path(dir, chan);
+
+ const bool is_lo1_enabled = _tree->access<bool>(fe_path / ZBX_LO1 / "enabled").get();
+ const bool is_lo1_locked =
+ _lo_ctrl_map.at(zbx_lo_ctrl::lo_string_to_enum(dir, chan, ZBX_LO1))
+ ->get_lock_status();
+ // LO2 is always enabled via center frequency tuning, but users may manually disable
+ // it
+ const bool is_lo2_enabled = _tree->access<bool>(fe_path / ZBX_LO2 / "enabled").get();
+ const bool is_lo2_locked =
+ _lo_ctrl_map.at(zbx_lo_ctrl::lo_string_to_enum(dir, chan, ZBX_LO2))
+ ->get_lock_status();
+ // We only care about the lock status if it's enabled (lowband center frequency)
+ // That means we have set it to true if is_lo[1,2]_enabled is *false*, but check for
+ // the lock if is_lo[1,2]_enabled is *true*
+ return (!is_lo1_enabled || is_lo1_locked) && (!is_lo2_enabled || is_lo2_locked);
+}
+
+fs_path zbx_dboard_impl::_get_frontend_path(
+ const direction_t dir, const size_t chan_idx) const
+{
+ UHD_ASSERT_THROW(chan_idx < ZBX_NUM_CHANS);
+ const std::string frontend = dir == TX_DIRECTION ? "tx_frontends" : "rx_frontends";
+ return fs_path("dboard") / frontend / chan_idx;
+}
+
+std::vector<uhd::usrp::pwr_cal_mgr::sptr>& zbx_dboard_impl::get_pwr_mgr(
+ uhd::direction_t trx)
+{
+ switch (trx) {
+ case uhd::RX_DIRECTION:
+ return _rx_pwr_mgr;
+ case uhd::TX_DIRECTION:
+ return _tx_pwr_mgr;
+ default:
+ UHD_THROW_INVALID_CODE_PATH();
+ }
+}
+
+}}} // namespace uhd::usrp::zbx
diff --git a/host/lib/usrp/dboard/zbx/zbx_dboard_init.cpp b/host/lib/usrp/dboard/zbx/zbx_dboard_init.cpp
new file mode 100644
index 000000000..e6bbf2798
--- /dev/null
+++ b/host/lib/usrp/dboard/zbx/zbx_dboard_init.cpp
@@ -0,0 +1,685 @@
+//
+// Copyright 2020 Ettus Research, a National Instruments Brand
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+#include <uhd/cal/database.hpp>
+#include <uhd/exception.hpp>
+#include <uhd/property_tree.hpp>
+#include <uhd/property_tree.ipp>
+#include <uhd/rfnoc/register_iface.hpp>
+#include <uhd/transport/chdr.hpp>
+#include <uhd/types/direction.hpp>
+#include <uhd/types/eeprom.hpp>
+#include <uhd/types/ranges.hpp>
+#include <uhd/types/sensors.hpp>
+#include <uhd/utils/log.hpp>
+#include <uhdlib/experts/expert_container.hpp>
+#include <uhdlib/experts/expert_factory.hpp>
+#include <uhdlib/rfnoc/reg_iface_adapter.hpp>
+#include <uhdlib/usrp/dboard/zbx/zbx_constants.hpp>
+#include <uhdlib/usrp/dboard/zbx/zbx_dboard.hpp>
+#include <uhdlib/usrp/dboard/zbx/zbx_expert.hpp>
+#include <boost/algorithm/string.hpp>
+#include <sstream>
+#include <vector>
+
+using namespace uhd;
+using namespace uhd::experts;
+using namespace uhd::rfnoc;
+
+// ostream << operator overloads for our enum classes, so that property nodes of that type
+// can be added to our expert graph
+namespace uhd { namespace experts {
+
+std::ostream& operator<<(
+ std::ostream& os, const ::uhd::usrp::zbx::zbx_lo_source_t& lo_source)
+{
+ switch (lo_source) {
+ case ::uhd::usrp::zbx::zbx_lo_source_t::internal:
+ os << "internal";
+ return os;
+ case ::uhd::usrp::zbx::zbx_lo_source_t::external:
+ os << "external";
+ return os;
+ default:
+ UHD_THROW_INVALID_CODE_PATH();
+ }
+}
+
+std::ostream& operator<<(
+ std::ostream& os, const ::uhd::usrp::zbx::zbx_cpld_ctrl::atr_mode& atr)
+{
+ switch (atr) {
+ case ::uhd::usrp::zbx::zbx_cpld_ctrl::atr_mode::SW_DEFINED:
+ os << "SW_DEFINED";
+ return os;
+ case ::uhd::usrp::zbx::zbx_cpld_ctrl::atr_mode::CLASSIC_ATR:
+ os << "CLASSIC ATR";
+ return os;
+ case ::uhd::usrp::zbx::zbx_cpld_ctrl::atr_mode::FPGA_STATE:
+ os << "FPGA_STATE";
+ return os;
+ default:
+ UHD_THROW_INVALID_CODE_PATH();
+ }
+}
+}} // namespace uhd::experts
+
+namespace uhd { namespace usrp { namespace zbx {
+
+void zbx_dboard_impl::_init_cpld()
+{
+ // CPLD
+ RFNOC_LOG_TRACE("Initializing CPLD...");
+ _cpld = std::make_shared<zbx_cpld_ctrl>(
+ [this](
+ const uint32_t addr, const uint32_t data, const zbx_cpld_ctrl::chan_t chan) {
+ const auto time_spec = (chan == zbx_cpld_ctrl::NO_CHAN)
+ ? time_spec_t::ASAP
+ : (chan == zbx_cpld_ctrl::CHAN1)
+ ? _time_accessor(1)
+ : _time_accessor(0);
+ _regs.poke32(_reg_base_address + addr, data, time_spec);
+ },
+ [this](const uint32_t addr) {
+ // We don't do timed peeks, so no chan parameter here.
+ return _regs.peek32(_reg_base_address + addr);
+ },
+ [this](const uhd::time_spec_t& sleep_time) { _regs.sleep(sleep_time); },
+ get_unique_id() + "::CPLD");
+ UHD_ASSERT_THROW(_cpld);
+ // We don't have access to the scratch register, so we use the config
+ // registers to test communication. This also does some basic sanity check
+ // of the CPLDs logic.
+ RFNOC_LOG_TRACE("Testing CPLD communication...");
+ const uint32_t random_value = static_cast<uint32_t>(time(NULL));
+ _cpld->set_scratch(random_value);
+ UHD_ASSERT_THROW(_cpld->get_scratch() == random_value);
+ // Now go to classic ATR mode
+ RFNOC_LOG_TRACE("CPLD communication good. Switching to classic ATR mode.");
+ for (size_t i = 0; i < ZBX_NUM_CHANS; ++i) {
+ _cpld->set_atr_mode(
+ i, zbx_cpld_ctrl::atr_mode_target::DSA, zbx_cpld_ctrl::atr_mode::CLASSIC_ATR);
+ _cpld->set_atr_mode(i,
+ zbx_cpld_ctrl::atr_mode_target::PATH_LED,
+ zbx_cpld_ctrl::atr_mode::CLASSIC_ATR);
+ }
+}
+
+void zbx_dboard_impl::_init_peripherals()
+{
+ RFNOC_LOG_TRACE("Initializing peripherals...");
+ // Load DSA cal data (rx and tx)
+ constexpr char dsa_step_filename_tx[] = "zbx_dsa_tx";
+ constexpr char dsa_step_filename_rx[] = "zbx_dsa_rx";
+ uhd::eeprom_map_t eeprom_map = get_db_eeprom();
+ const std::string db_serial(eeprom_map["serial"].begin(), eeprom_map["serial"].end());
+ if (uhd::usrp::cal::database::has_cal_data(
+ dsa_step_filename_tx, db_serial, uhd::usrp::cal::source::ANY)) {
+ RFNOC_LOG_TRACE("load binary TX DSA steps from database...");
+ const auto tx_dsa_data = uhd::usrp::cal::database::read_cal_data(
+ dsa_step_filename_tx, db_serial, uhd::usrp::cal::source::ANY);
+ RFNOC_LOG_TRACE("create TX DSA object...");
+ _tx_dsa_cal = uhd::usrp::cal::zbx_tx_dsa_cal::make();
+ RFNOC_LOG_TRACE("store deserialized TX DSA data into object...");
+ _tx_dsa_cal->deserialize(tx_dsa_data);
+ } else {
+ RFNOC_LOG_ERROR("Could not find TX DSA cal data!");
+ throw uhd::runtime_error("Could not find TX DSA cal data!");
+ }
+ if (uhd::usrp::cal::database::has_cal_data(
+ dsa_step_filename_rx, db_serial, uhd::usrp::cal::source::ANY)) {
+ // read binary blob without knowledge about content
+ RFNOC_LOG_TRACE("load binary RX DSA steps from database...");
+ const auto rx_dsa_data = uhd::usrp::cal::database::read_cal_data(
+ dsa_step_filename_rx, db_serial, uhd::usrp::cal::source::ANY);
+
+ RFNOC_LOG_TRACE("create RX DSA object...");
+ _rx_dsa_cal = uhd::usrp::cal::zbx_rx_dsa_cal::make();
+
+ RFNOC_LOG_TRACE("store deserialized RX DSA data into object...");
+ _rx_dsa_cal->deserialize(rx_dsa_data);
+ } else {
+ RFNOC_LOG_ERROR("Could not find RX DSA cal data!");
+ throw uhd::runtime_error("Could not find RX DSA cal data!");
+ }
+}
+
+void zbx_dboard_impl::_init_prop_tree()
+{
+ auto subtree = get_tree()->subtree(fs_path("dboard"));
+
+ // Construct RX frontend
+ for (size_t chan_idx = 0; chan_idx < ZBX_NUM_CHANS; chan_idx++) {
+ const fs_path fe_path = fs_path("rx_frontends") / chan_idx;
+
+ // Command time needs to be shadowed into the property tree so we can use
+ // it in the expert graph. TX and RX share the command time, so we could
+ // put it onto its own sub-tree, or copy the property between TX and RX.
+ // With respect to TwinRX and trying to keep the tree lean and browsable,
+ // we compromise and put the command time onto the RX frontend path, even
+ // though it's also valid for TX.
+ // This data node will be used for scheduling the other experts:
+ expert_factory::add_data_node<time_spec_t>(
+ _expert_container, fe_path / "time/fe", time_spec_t(0.0));
+ // This prop node will be used to import the command time into the
+ // graph:
+ expert_factory::add_prop_node<time_spec_t>(
+ _expert_container, subtree, fe_path / "time/cmd", time_spec_t(0.0));
+
+ _init_frontend_subtree(subtree, RX_DIRECTION, chan_idx, fe_path);
+
+ // The time nodes get connected with one scheduling expert per channel:
+ expert_factory::add_worker_node<zbx_scheduling_expert>(
+ _expert_container, _expert_container->node_retriever(), fe_path);
+ }
+
+ // Construct TX frontend
+ // Note: the TX frontend uses the RX property tree, this must
+ // be constructed after the RX frontend
+ for (size_t chan_idx = 0; chan_idx < ZBX_NUM_CHANS; chan_idx++) {
+ const fs_path fe_path = fs_path("tx_frontends") / chan_idx;
+ _init_frontend_subtree(subtree, TX_DIRECTION, chan_idx, fe_path);
+ }
+
+ // Now add the sync worker:
+ expert_factory::add_worker_node<zbx_sync_expert>(_expert_container,
+ _expert_container->node_retriever(),
+ fs_path("tx_frontends"),
+ fs_path("rx_frontends"),
+ _rfdcc,
+ _cpld);
+
+ subtree->create<eeprom_map_t>("eeprom")
+ .add_coerced_subscriber([this](const eeprom_map_t&) {
+ throw uhd::runtime_error("Attempting to update daughterboard eeprom!");
+ })
+ .set_publisher([this]() { return get_db_eeprom(); });
+}
+
+void zbx_dboard_impl::_init_frontend_subtree(uhd::property_tree::sptr subtree,
+ const uhd::direction_t trx,
+ const size_t chan_idx,
+ const fs_path fe_path)
+{
+ static constexpr char ZBX_FE_NAME[] = "ZBX";
+
+ RFNOC_LOG_TRACE("Adding non-RFNoC block properties for channel "
+ << chan_idx << " to prop tree path " << fe_path);
+ // Standard attributes
+ subtree->create<std::string>(fe_path / "name").set(ZBX_FE_NAME);
+ subtree->create<std::string>(fe_path / "connection").set("IQ");
+
+ _init_frequency_prop_tree(subtree, _expert_container, fe_path);
+ _init_gain_prop_tree(subtree, _expert_container, trx, chan_idx, fe_path);
+ _init_antenna_prop_tree(subtree, _expert_container, trx, chan_idx, fe_path);
+ _init_lo_prop_tree(subtree, _expert_container, trx, chan_idx, fe_path);
+ _init_programming_prop_tree(subtree, _expert_container, fe_path);
+ _init_experts(subtree, _expert_container, trx, chan_idx, fe_path);
+}
+
+
+uhd::usrp::pwr_cal_mgr::sptr zbx_dboard_impl::_init_power_cal(
+ uhd::property_tree::sptr subtree,
+ const uhd::direction_t trx,
+ const size_t chan_idx,
+ const fs_path fe_path)
+{
+ const std::string DIR = (trx == TX_DIRECTION) ? "TX" : "RX";
+
+ uhd::eeprom_map_t eeprom_map = get_db_eeprom();
+ /* The cal serial is the DB serial plus the FE name */
+ const std::string db_serial(eeprom_map["serial"].begin(), eeprom_map["serial"].end());
+ const std::string cal_serial =
+ db_serial + "#" + subtree->access<std::string>(fe_path / "name").get();
+ /* Now create a gain group for this. */
+ /* _?x_gain_groups won't work, because it doesn't group the */
+ /* gains the way we want them to be grouped. */
+ auto ggroup = uhd::gain_group::make();
+ ggroup->register_fcns(HW_GAIN_STAGE,
+ {[this, trx, chan_idx]() {
+ return trx == TX_DIRECTION ? get_tx_gain_range(chan_idx)
+ : get_rx_gain_range(chan_idx);
+ },
+ [this, trx, chan_idx]() {
+ return trx == TX_DIRECTION ? get_tx_gain(ZBX_GAIN_STAGE_ALL, chan_idx)
+ : get_rx_gain(ZBX_GAIN_STAGE_ALL, chan_idx);
+ },
+ [this, trx, chan_idx](const double gain) {
+ trx == TX_DIRECTION ? set_tx_gain(gain, chan_idx)
+ : set_rx_gain(gain, chan_idx);
+ }},
+ 10 /* High priority */);
+ /* If we had a digital (baseband) gain, we would register it here,*/
+ /* so that the power manager would know to use it as a */
+ /* backup gain stage. */
+ /* Note that such a baseband gain might not be available */
+ /* on the LV version. */
+ return uhd::usrp::pwr_cal_mgr::make(
+ cal_serial,
+ "X400-CAL-" + DIR,
+ [this, trx, chan_idx]() {
+ return trx == TX_DIRECTION ? get_tx_frequency(chan_idx)
+ : get_rx_frequency(chan_idx);
+ },
+ [this,
+ trx_str = (trx == TX_DIRECTION ? "tx" : "rx"),
+ fe_path,
+ subtree,
+ chan_str = std::to_string(chan_idx)]() -> std::string {
+ const std::string antenna = pwr_cal_mgr::sanitize_antenna_name(
+ subtree->access<std::string>(fe_path / "antenna/value").get());
+ // The lookup key for X410 + ZBX shall start with x4xx_pwr_zbx.
+ // Should we rev the ZBX in a way that would make generic cal data
+ // unsuitable between revs, then we need to check the rev (or PID)
+ // here and generate a different key prefix (e.g. x4xx_pwr_zbxD_ or
+ // something like that).
+ return std::string("x4xx_pwr_zbx_") + trx_str + "_" + chan_str + "_"
+ + antenna;
+ },
+ ggroup);
+}
+
+void zbx_dboard_impl::_init_experts(uhd::property_tree::sptr subtree,
+ expert_container::sptr expert,
+ const uhd::direction_t trx,
+ const size_t chan_idx,
+ const fs_path fe_path)
+{
+ RFNOC_LOG_TRACE(fe_path + ", Creating experts...");
+
+ get_pwr_mgr(trx).insert(get_pwr_mgr(trx).begin() + chan_idx,
+ _init_power_cal(subtree, trx, chan_idx, fe_path));
+
+ // NOTE: THE ORDER OF EXPERT INITIALIZATION MATTERS
+ // After construction, all nodes (properties and experts) are marked dirty. Any
+ // subsequent calls to the container will trigger a resolve_all(), in which case
+ // the nodes are all resolved in REVERSE ORDER of construction, like a stack. With
+ // that in mind, we have to initialize the experts in line with that reverse order,
+ // because some experts rely on each other's construction/resolution to avoid
+ // errors (e.g., gain expert's dsa_cal is dependant on frequency be's coerced
+ // frequency, which is nan on dual_prop_node construction) After construction and
+ // subsequent resolution, the nodes will follow simple topological ruling as long
+ // as we only change one property at a time.
+
+ // The current order should be:
+ // Frequency FE Expert -> LO Expert(s) -> MPM Expert -> Frequency BE Expert -> Gain
+ // Expert -> Programming Expert
+
+ if (trx == TX_DIRECTION) {
+ expert_factory::add_worker_node<zbx_tx_programming_expert>(expert,
+ expert->node_retriever(),
+ fe_path,
+ fs_path("rx_frontends") / chan_idx,
+ chan_idx,
+ _tx_dsa_cal,
+ _cpld);
+
+ expert_factory::add_worker_node<zbx_tx_gain_expert>(expert,
+ expert->node_retriever(),
+ fe_path,
+ chan_idx,
+ get_pwr_mgr(trx).at(chan_idx),
+ _tx_dsa_cal);
+ } else {
+ expert_factory::add_worker_node<zbx_rx_programming_expert>(
+ expert, expert->node_retriever(), fe_path, chan_idx, _rx_dsa_cal, _cpld);
+
+ expert_factory::add_worker_node<zbx_rx_gain_expert>(expert,
+ expert->node_retriever(),
+ fe_path,
+ chan_idx,
+ get_pwr_mgr(trx).at(chan_idx),
+ _rx_dsa_cal);
+ }
+
+ expert_factory::add_worker_node<zbx_freq_be_expert>(
+ expert, expert->node_retriever(), fe_path, trx, chan_idx);
+
+ expert_factory::add_worker_node<zbx_band_inversion_expert>(
+ expert, expert->node_retriever(), fe_path, trx, chan_idx, _db_idx, _rpcc);
+
+
+ // Initialize our LO Control Experts
+ for (auto lo_select : ZBX_LOS) {
+ if (lo_select == RFDC_NCO) {
+ expert_factory::add_worker_node<zbx_rfdc_freq_expert>(expert,
+ expert->node_retriever(),
+ fe_path,
+ trx,
+ chan_idx,
+ _rpc_prefix,
+ _db_idx,
+ _mb_rpcc);
+ } else {
+ const zbx_lo_t lo = zbx_lo_ctrl::lo_string_to_enum(trx, chan_idx, lo_select);
+ std::shared_ptr<zbx_lo_ctrl> lo_ctrl = std::make_shared<zbx_lo_ctrl>(
+ lo,
+ [this, lo](const uint32_t addr, const uint16_t data) {
+ _cpld->lo_poke16(lo, addr, data);
+ },
+ [this, lo](const uint32_t addr) { return _cpld->lo_peek16(lo, addr); },
+ [this](const uhd::time_spec_t& sleep_time) { _regs.sleep(sleep_time); },
+ LMX2572_DEFAULT_FREQ,
+ _prc_rate,
+ false);
+ expert_factory::add_worker_node<zbx_lo_expert>(expert,
+ expert->node_retriever(),
+ fe_path,
+ trx,
+ chan_idx,
+ lo_select,
+ lo_ctrl);
+ _lo_ctrl_map.insert({lo, lo_ctrl});
+ }
+ }
+
+ const double lo_step_size = _prc_rate / ZBX_RELATIVE_LO_STEP_SIZE;
+ RFNOC_LOG_DEBUG("LO step size: " << (lo_step_size / 1e6) << " MHz.")
+ expert_factory::add_worker_node<zbx_freq_fe_expert>(expert,
+ expert->node_retriever(),
+ fe_path,
+ trx,
+ chan_idx,
+ _rfdc_rate,
+ lo_step_size);
+ RFNOC_LOG_TRACE(fe_path + ", Experts created");
+}
+
+void zbx_dboard_impl::_init_frequency_prop_tree(uhd::property_tree::sptr subtree,
+ expert_container::sptr expert,
+ const fs_path fe_path)
+{
+ expert_factory::add_dual_prop_node<double>(
+ expert, subtree, fe_path / "freq", ZBX_DEFAULT_FREQ, AUTO_RESOLVE_ON_WRITE);
+ expert_factory::add_dual_prop_node<double>(
+ expert, subtree, fe_path / "if_freq", 0.0, AUTO_RESOLVE_ON_WRITE);
+ expert_factory::add_data_node<bool>(expert, fe_path / "is_highband", false);
+ expert_factory::add_data_node<int>(
+ expert, fe_path / "mixer1_m", 0, AUTO_RESOLVE_ON_WRITE);
+ expert_factory::add_data_node<int>(
+ expert, fe_path / "mixer1_n", 0, AUTO_RESOLVE_ON_WRITE);
+ expert_factory::add_data_node<int>(
+ expert, fe_path / "mixer2_m", 0, AUTO_RESOLVE_ON_WRITE);
+ expert_factory::add_data_node<int>(
+ expert, fe_path / "mixer2_n", 0, AUTO_RESOLVE_ON_WRITE);
+ expert_factory::add_data_node<bool>(
+ expert, fe_path / "band_inverted", false, AUTO_RESOLVE_ON_WRITE);
+
+ subtree->create<double>(fe_path / "bandwidth" / "value")
+ .set(ZBX_DEFAULT_BANDWIDTH)
+ .set_coercer([](const double) { return ZBX_DEFAULT_BANDWIDTH; });
+ subtree->create<meta_range_t>(fe_path / "bandwidth" / "range")
+ .set({ZBX_DEFAULT_BANDWIDTH, ZBX_DEFAULT_BANDWIDTH})
+ .set_coercer([](const meta_range_t&) {
+ return meta_range_t(ZBX_DEFAULT_BANDWIDTH, ZBX_DEFAULT_BANDWIDTH);
+ });
+ subtree->create<meta_range_t>(fe_path / "freq" / "range")
+ .set(ZBX_FREQ_RANGE)
+ .add_coerced_subscriber([](const meta_range_t&) {
+ throw uhd::runtime_error("Attempting to update freq range!");
+ });
+}
+
+void zbx_dboard_impl::_init_gain_prop_tree(uhd::property_tree::sptr subtree,
+ expert_container::sptr expert,
+ const uhd::direction_t trx,
+ const size_t chan_idx,
+ const fs_path fe_path)
+{
+ // First, overall gain nodes
+ const auto gain_base_path = fe_path / "gains";
+ expert_factory::add_dual_prop_node<double>(expert,
+ subtree,
+ gain_base_path / ZBX_GAIN_STAGE_ALL / "value",
+ trx == TX_DIRECTION ? TX_MIN_GAIN : RX_MIN_GAIN,
+ AUTO_RESOLVE_ON_WRITE);
+ subtree->create<meta_range_t>(fe_path / "gains" / "all" / "range")
+ .add_coerced_subscriber([](const meta_range_t&) {
+ throw uhd::runtime_error("Attempting to update gain range!");
+ })
+ .set_publisher([this, trx, chan_idx]() {
+ return (trx == TX_DIRECTION) ? this->get_tx_gain_range(chan_idx)
+ : this->get_rx_gain_range(chan_idx);
+ });
+ // Then, individual DSA/amp gain nodes
+ if (trx == TX_DIRECTION) {
+ // DSAs
+ for (const auto dsa : {ZBX_GAIN_STAGE_DSA1, ZBX_GAIN_STAGE_DSA2}) {
+ const auto gain_path = gain_base_path / dsa;
+ expert_factory::add_dual_prop_node<double>(
+ expert, subtree, gain_path / "value", 0, AUTO_RESOLVE_ON_WRITE);
+ subtree->create<meta_range_t>(gain_path / "range")
+ .set(uhd::meta_range_t(0, ZBX_TX_DSA_MAX_ATT, 1.0));
+ expert_factory::add_worker_node<zbx_gain_coercer_expert>(_expert_container,
+ _expert_container->node_retriever(),
+ gain_path / "value",
+ uhd::meta_range_t(0, ZBX_TX_DSA_MAX_ATT, 1.0));
+ }
+ // Amp
+ const auto amp_path = gain_base_path / ZBX_GAIN_STAGE_AMP;
+ expert_factory::add_dual_prop_node<double>(expert,
+ subtree,
+ amp_path / "value",
+ ZBX_TX_LOWBAND_GAIN,
+ AUTO_RESOLVE_ON_WRITE);
+ uhd::meta_range_t amp_gain_range;
+ for (const auto tx_gain_pair : ZBX_TX_GAIN_AMP_MAP) {
+ amp_gain_range.push_back(uhd::range_t(tx_gain_pair.first));
+ }
+ subtree->create<meta_range_t>(amp_path / "range").set(amp_gain_range);
+ expert_factory::add_worker_node<zbx_gain_coercer_expert>(_expert_container,
+ _expert_container->node_retriever(),
+ amp_path / "value",
+ amp_gain_range);
+ } else {
+ // RX only has DSAs
+ for (const auto dsa : {ZBX_GAIN_STAGE_DSA1,
+ ZBX_GAIN_STAGE_DSA2,
+ ZBX_GAIN_STAGE_DSA3A,
+ ZBX_GAIN_STAGE_DSA3B}) {
+ const auto gain_path = gain_base_path / dsa;
+ expert_factory::add_dual_prop_node<double>(
+ expert, subtree, gain_path / "value", 0, AUTO_RESOLVE_ON_WRITE);
+ subtree->create<meta_range_t>(gain_path / "range")
+ .set(uhd::meta_range_t(0, ZBX_RX_DSA_MAX_ATT, 1.0));
+ expert_factory::add_worker_node<zbx_gain_coercer_expert>(_expert_container,
+ _expert_container->node_retriever(),
+ gain_path / "value",
+ uhd::meta_range_t(0, ZBX_RX_DSA_MAX_ATT, 1.0));
+ }
+ }
+
+ const uhd::fs_path gain_profile_path = gain_base_path / "all" / "profile";
+ expert_factory::add_prop_node<std::string>(expert,
+ subtree,
+ gain_profile_path,
+ ZBX_GAIN_PROFILE_DEFAULT,
+ AUTO_RESOLVE_ON_WRITE);
+ auto& gain_profile = (trx == TX_DIRECTION) ? _tx_gain_profile_api
+ : _rx_gain_profile_api;
+ auto& other_dir_gp = (trx == TX_DIRECTION) ? _rx_gain_profile_api
+ : _tx_gain_profile_api;
+ auto gain_profile_subscriber = [this, other_dir_gp, trx](
+ const std::string& profile, const size_t chan) {
+ // Upon changing the gain profile, we need to import the new value into
+ // the property tree.
+ const auto path = fs_path("dboard")
+ / (trx == TX_DIRECTION ? "tx_frontends" : "rx_frontends") / chan
+ / "gains" / "all" / "profile";
+ get_tree()->access<std::string>(path).set(profile);
+ // The CPLD does not have the option to have different ATR modes for RX
+ // and TX (it does have different modes for channel 0 and 1 though).
+ // This means we have to match up the gain profiles between RX and TX.
+ // The ZBX_GAIN_PROFILE_CPLD_NOATR profile uses the SW_DEFINED mode,
+ // and all the others use CLASSIC_ATR. So either both match
+ // ZBX_GAIN_PROFILE_CPLD_NOATR, or none do.
+ // This will not cause a loop, because the other_dir_gp will already
+ // match this one by the time we call it.
+ if ((profile == ZBX_GAIN_PROFILE_CPLD_NOATR
+ && other_dir_gp->get_gain_profile(chan) != ZBX_GAIN_PROFILE_CPLD_NOATR)
+ || (profile != ZBX_GAIN_PROFILE_CPLD_NOATR
+ && other_dir_gp->get_gain_profile(chan) == ZBX_GAIN_PROFILE_CPLD_NOATR)) {
+ RFNOC_LOG_DEBUG("Channel " << chan << ": Setting gain profile to `" << profile
+ << "' for both TX and RX.");
+ other_dir_gp->set_gain_profile(profile, chan);
+ }
+ };
+
+ gain_profile->add_subscriber(std::move(gain_profile_subscriber));
+}
+
+void zbx_dboard_impl::_init_antenna_prop_tree(uhd::property_tree::sptr subtree,
+ expert_container::sptr expert,
+ const uhd::direction_t trx,
+ const size_t chan_idx,
+ const fs_path fe_path)
+{
+ const std::string default_ant = trx == TX_DIRECTION ? DEFAULT_TX_ANTENNA
+ : DEFAULT_RX_ANTENNA;
+ expert_factory::add_prop_node<std::string>(expert,
+ subtree,
+ fe_path / "antenna" / "value",
+ default_ant,
+ AUTO_RESOLVE_ON_WRITE);
+ subtree->access<std::string>(fe_path / "antenna" / "value")
+ .set_coercer([trx](const std::string& ant_name) {
+ const auto ant_map = trx == TX_DIRECTION ? TX_ANTENNA_NAME_COMPAT_MAP
+ : RX_ANTENNA_NAME_COMPAT_MAP;
+ return ant_map.count(ant_name) ? ant_map.at(ant_name) : ant_name;
+ });
+ subtree->create<std::vector<std::string>>(fe_path / "antenna" / "options")
+ .set(trx == TX_DIRECTION ? get_tx_antennas(chan_idx) : get_rx_antennas(chan_idx))
+ .add_coerced_subscriber([](const std::vector<std::string>&) {
+ throw uhd::runtime_error("Attempting to update antenna options!");
+ });
+}
+
+void zbx_dboard_impl::_init_programming_prop_tree(uhd::property_tree::sptr subtree,
+ expert_container::sptr expert,
+ const fs_path fe_path)
+{
+ expert_factory::add_prop_node<int>(
+ expert, subtree, fe_path / "rf" / "filter", 1, AUTO_RESOLVE_ON_WRITE);
+ expert_factory::add_prop_node<int>(
+ expert, subtree, fe_path / "if1" / "filter", 1, AUTO_RESOLVE_ON_WRITE);
+ expert_factory::add_prop_node<int>(
+ expert, subtree, fe_path / "if2" / "filter", 1, AUTO_RESOLVE_ON_WRITE);
+ expert_factory::add_prop_node<zbx_cpld_ctrl::atr_mode>(expert,
+ subtree,
+ fe_path / "atr_mode",
+ zbx_cpld_ctrl::atr_mode::CLASSIC_ATR,
+ AUTO_RESOLVE_ON_WRITE);
+}
+
+void zbx_dboard_impl::_init_lo_prop_tree(uhd::property_tree::sptr subtree,
+ expert_container::sptr expert,
+ const uhd::direction_t trx,
+ const size_t chan_idx,
+ const fs_path fe_path)
+{
+ // Analog LO Specific
+ for (const std::string lo : {ZBX_LO1, ZBX_LO2}) {
+ expert_factory::add_prop_node<zbx_lo_source_t>(expert,
+ subtree,
+ fe_path / "ch" / lo / "source",
+ ZBX_DEFAULT_LO_SOURCE,
+ AUTO_RESOLVE_ON_WRITE);
+ expert_factory::add_prop_node<bool>(
+ expert, subtree, fe_path / lo / "enabled", false, AUTO_RESOLVE_ON_WRITE);
+ expert_factory::add_prop_node<bool>(
+ expert, subtree, fe_path / lo / "test_mode", false, AUTO_RESOLVE_ON_WRITE);
+ expert_factory::add_dual_prop_node<double>(expert,
+ subtree,
+ fe_path / "los" / lo / "freq" / "value",
+ LMX2572_DEFAULT_FREQ,
+ AUTO_RESOLVE_ON_WRITE);
+
+ subtree->create<meta_range_t>(fe_path / "los" / lo / "freq/range")
+ .set_publisher(
+ [this, lo, chan_idx]() { return this->_get_lo_freq_range(lo, chan_idx); })
+ .add_coerced_subscriber([](const meta_range_t&) {
+ throw uhd::runtime_error("Attempting to update freq range!");
+ });
+ subtree->create<std::vector<std::string>>(fe_path / "los" / lo / "source/options")
+ .set_publisher([this, lo, trx, chan_idx]() {
+ return trx == TX_DIRECTION ? this->get_tx_lo_sources(lo, chan_idx)
+ : this->get_rx_lo_sources(lo, chan_idx);
+ })
+ .add_coerced_subscriber([](const std::vector<std::string>&) {
+ throw uhd::runtime_error("Attempting to update LO source options!");
+ });
+
+ subtree
+ ->create<sensor_value_t>(
+ fe_path / "sensors" / boost::algorithm::to_lower_copy(lo) + "_locked")
+ .add_coerced_subscriber([](const sensor_value_t&) {
+ throw uhd::runtime_error("Attempting to write to sensor!");
+ })
+ .set_publisher([this, lo, trx, chan_idx]() {
+ return sensor_value_t(lo,
+ this->_lo_ctrl_map
+ .at(zbx_lo_ctrl::lo_string_to_enum(trx, chan_idx, lo))
+ ->get_lock_status(),
+ "locked",
+ "unlocked");
+ });
+ }
+
+ // The NCO gets a sub-node called 'reset'. It is read/write: Write will
+ // perform a reset, and read will return the reset status. The latter is
+ // also returned in the 'locked' sensor for the NCO, but the 'nco_locked'
+ // sensor node is read-only, and returns a sensor_value_t (not a bool).
+ // This node is primarily used for debugging, but can also serve as a manual
+ // reset line for the NCOs.
+ const auto nco = (trx == TX_DIRECTION)
+ ? (chan_idx == 0 ? rfdc_control::rfdc_type::TX0
+ : rfdc_control::rfdc_type::TX1)
+ : (chan_idx == 0 ? rfdc_control::rfdc_type::RX0
+ : rfdc_control::rfdc_type::RX1);
+ subtree->create<bool>(fe_path / "los" / RFDC_NCO / "reset")
+ .set_publisher([this]() { return this->_rfdcc->get_nco_reset_done(); })
+ .add_coerced_subscriber([this, nco, chan_idx](const bool&) {
+ RFNOC_LOG_TRACE("Resetting NCO " << size_t(nco) << ", chan " << chan_idx);
+ this->_rfdcc->reset_ncos({nco}, this->_time_accessor(chan_idx));
+ });
+
+ expert_factory::add_dual_prop_node<double>(expert,
+ subtree,
+ fe_path / "los" / RFDC_NCO / "freq" / "value",
+ // Initialize with current value
+ _mb_rpcc->rfdc_get_nco_freq(trx == TX_DIRECTION ? "tx" : "rx", _db_idx, chan_idx),
+ AUTO_RESOLVE_ON_WRITE);
+
+ expert_factory::add_prop_node<zbx_lo_source_t>(expert,
+ subtree,
+ fe_path / "ch" / RFDC_NCO / "source",
+ ZBX_DEFAULT_LO_SOURCE,
+ AUTO_RESOLVE_ON_WRITE);
+
+ // LO lock sensor
+ // We can't make this its own property value because it has to have access to two
+ // containers (two instances of zbx lo expert)
+ subtree->create<sensor_value_t>(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, trx, chan_idx]() {
+ return sensor_value_t("all_los",
+ this->_get_all_los_locked(trx, chan_idx),
+ "locked",
+ "unlocked");
+ });
+ subtree->create<sensor_value_t>(fe_path / "sensors" / "nco_locked")
+ .add_coerced_subscriber([](const sensor_value_t&) {
+ throw uhd::runtime_error("Attempting to write to sensor!");
+ })
+ .set_publisher([this]() {
+ return sensor_value_t(
+ RFDC_NCO, this->_rfdcc->get_nco_reset_done(), "locked", "unlocked");
+ });
+}
+}}} // namespace uhd::usrp::zbx
diff --git a/host/lib/usrp/dboard/zbx/zbx_expert.cpp b/host/lib/usrp/dboard/zbx/zbx_expert.cpp
new file mode 100644
index 000000000..79e13e230
--- /dev/null
+++ b/host/lib/usrp/dboard/zbx/zbx_expert.cpp
@@ -0,0 +1,672 @@
+//
+// Copyright 2020 Ettus Research, a National Instruments Brand
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+#include <uhd/utils/assert_has.hpp>
+#include <uhd/utils/log.hpp>
+#include <uhd/utils/math.hpp>
+#include <uhdlib/usrp/dboard/zbx/zbx_expert.hpp>
+#include <uhdlib/utils/interpolation.hpp>
+#include <uhdlib/utils/narrow.hpp>
+#include <algorithm>
+#include <array>
+
+using namespace uhd;
+
+namespace uhd { namespace usrp { namespace zbx {
+
+namespace {
+
+/*********************************************************************
+ * Misc/calculative helper functions
+ **********************************************************************/
+bool _is_band_highband(const tune_map_item_t tune_setting)
+{
+ // Lowband frequency paths do not utilize an RF filter
+ return tune_setting.rf_fir == 0;
+}
+
+tune_map_item_t _get_tune_settings(const double freq, const uhd::direction_t trx)
+{
+ auto tune_setting = trx == RX_DIRECTION ? rx_tune_map.begin() : tx_tune_map.begin();
+
+ auto tune_settings_end = trx == RX_DIRECTION ? rx_tune_map.end() : tx_tune_map.end();
+
+ for (; tune_setting != tune_settings_end; ++tune_setting) {
+ if (tune_setting->max_band_freq >= freq) {
+ return *tune_setting;
+ }
+ }
+ // Didn't find a tune setting. This frequency should have been clipped, this is an
+ // internal error.
+ UHD_THROW_INVALID_CODE_PATH();
+}
+
+bool _is_band_inverted(const uhd::direction_t trx,
+ const double if2_freq,
+ const double rfdc_rate,
+ const tune_map_item_t tune_setting)
+{
+ const bool is_if2_nyquist2 = if2_freq > (rfdc_rate / 2);
+
+ // We count the number of inversions introduced by the signal chain, starting
+ // at the RFDC
+ const int num_inversions =
+ // If we're in the second Nyquist zone, we're inverted
+ int(is_if2_nyquist2) +
+ // LO2 mixer may invert
+ int(tune_setting.mix2_m == -1) +
+ // LO1 mixer can only invert in the lowband
+ int(!_is_band_highband(tune_setting) && tune_setting.mix1_m == -1);
+
+ // In the RX direction, an extra inversion is needed
+ // TODO: We don't know where this is coming from
+ const bool num_inversions_is_odd = num_inversions % 2 != 0;
+ if (trx == RX_DIRECTION) {
+ return !num_inversions_is_odd;
+ } else {
+ return num_inversions_is_odd;
+ }
+}
+
+double _calc_lo2_freq(
+ const double if1_freq, const double if2_freq, const int mix2_m, const int mix2_n)
+{
+ return (if2_freq - (mix2_m * if1_freq)) / mix2_n;
+}
+
+double _calc_if2_freq(
+ const double if1_freq, const double lo2_freq, const int mix2_m, const int mix2_n)
+{
+ return mix2_n * lo2_freq + mix2_m * if1_freq;
+}
+
+std::string _get_trx_string(const direction_t dir)
+{
+ if (dir == RX_DIRECTION) {
+ return "rx";
+ } else if (dir == TX_DIRECTION) {
+ return "tx";
+ } else {
+ UHD_THROW_INVALID_CODE_PATH();
+ }
+}
+
+// For various RF performance considerations (such as spur reduction), different bands
+// vary between using fixed IF1 and/or IF2 or using variable IF1 and/or IF2. Bands with a
+// fixed IF1/IF2 have ifX_freq_min == IFX_freq_max, and _calc_ifX_freq() will return that
+// single value. Bands with variable IF1/IF2 will shift the IFX based on where in the RF
+// band we are tuning by using linear interpolation. (if1 calculation takes place only if
+// tune frequency is lowband)
+double _calc_if1_freq(const double tune_freq, const tune_map_item_t tune_setting)
+{
+ if (tune_setting.if1_freq_min == tune_setting.if1_freq_max) {
+ return tune_setting.if1_freq_min;
+ }
+
+ return uhd::math::linear_interp(tune_freq,
+ tune_setting.min_band_freq,
+ tune_setting.if1_freq_min,
+ tune_setting.max_band_freq,
+ tune_setting.if1_freq_max);
+}
+
+double _calc_ideal_if2_freq(const double tune_freq, const tune_map_item_t tune_setting)
+{
+ // linear_interp() wants to interpolate and will throw if these are identical:
+ if (tune_setting.if2_freq_min == tune_setting.if2_freq_max) {
+ return tune_setting.if2_freq_min;
+ }
+
+ return uhd::math::linear_interp(tune_freq,
+ tune_setting.min_band_freq,
+ tune_setting.if2_freq_min,
+ tune_setting.max_band_freq,
+ tune_setting.if2_freq_max);
+}
+
+} // namespace
+
+/*!---------------------------------------------------------
+ * EXPERT RESOLVE FUNCTIONS
+ *
+ * This sections contains all expert resolve functions.
+ * These methods are triggered by any of the bound accessors becoming "dirty",
+ * or changing value
+ * --------------------------------------------------------
+ */
+void zbx_scheduling_expert::resolve()
+{
+ // We currently have no fancy scheduling, but here is where we'd add it if
+ // we need to do that (e.g., plan out SYNC pulse timing vs. NCO timing etc.)
+ _frontend_time = _command_time;
+}
+
+void zbx_freq_fe_expert::resolve()
+{
+ const double tune_freq = ZBX_FREQ_RANGE.clip(_desired_frequency);
+ _tune_settings = _get_tune_settings(tune_freq, _trx);
+
+ // Set mixer values so the backend expert knows how to calculate final frequency
+ _mixer1_m = _tune_settings.mix1_m;
+ _mixer1_n = _tune_settings.mix1_n;
+ _mixer2_m = _tune_settings.mix2_m;
+ _mixer2_n = _tune_settings.mix2_n;
+
+ _is_highband = _is_band_highband(_tune_settings);
+ _lo1_enabled = !_is_highband.get();
+
+ double if1_freq = tune_freq;
+ const double lo_step = _lo_freq_range.step();
+ // If we need to apply an offset to avoid injection locking, we need to
+ // offset in different directions for different channels on the same zbx
+ const double lo_offset_sign = (_chan == 0) ? -1 : 1;
+ // In high band, LO1 is not needed (the signal is already at a high enough
+ // frequency for the second stage)
+ if (_lo1_enabled) {
+ // Calculate the ideal IF1:
+ if1_freq = _calc_if1_freq(tune_freq, _tune_settings);
+ // We calculate the LO1 frequency by first shifting the tune frequency to the
+ // desired IF, and then applying an offset such that CH0 and CH1 tune to distinct
+ // LO1 frequencies: This is done to prevent the LO's from interfering with each
+ // other in a phenomenon known as injection locking.
+ const double lo1_freq =
+ if1_freq + (_tune_settings.mix1_n * tune_freq) + (lo_offset_sign * lo_step);
+ // Now, quantize the LO frequency to the nearest valid value:
+ _desired_lo1_frequency = _lo_freq_range.clip(lo1_freq, true);
+ // Because LO1 frequency probably changed during quantization, we simply
+ // re-calculate the now-valid IF1 (the following equation is the same as
+ // the LO1 frequency calculation, but solved for if1_freq):
+ if1_freq = _desired_lo1_frequency - (_tune_settings.mix1_n * tune_freq);
+ }
+
+ _lo2_enabled = true;
+ // Calculate ideal IF2 frequency:
+ const double if2_freq = _calc_ideal_if2_freq(tune_freq, _tune_settings);
+ // Calculate LO2 frequency from that:
+ _desired_lo2_frequency = _calc_lo2_freq(if1_freq, if2_freq, _mixer2_m, _mixer2_n);
+ // Similar to LO1, apply an offset such that CH0 and CH1 tune to distinct LO2
+ // frequencies to prevent potential interference between CH0 and CH1 LO2's from
+ // injection locking: In highband (LO1 disabled), this must explicitly be done below.
+ // In lowband (LO1 enabled), the LO1 will have already been shifted and, as a result,
+ // the LO2's will have already been shifted to compensate for LO1 in previous
+ // function. Note that in lowband, the LO1's and LO2's will be offset between CH0 and
+ // CH1; however, they will be offset in opposite direction such that the NCO frequency
+ // will be the same between CH0 and CH1. This is not the case for highband (only LO2
+ // and they must be offset).
+ if (!_lo1_enabled) {
+ _desired_lo2_frequency = _desired_lo2_frequency + (lo_offset_sign * lo_step);
+ }
+ // Now, quantize the LO frequency to the nearest valid value:
+ _desired_lo2_frequency = _lo_freq_range.clip(_desired_lo2_frequency, true);
+ // Calculate actual IF2 frequency from LO2 and IF1 frequencies:
+ _desired_if2_frequency =
+ _calc_if2_freq(if1_freq, _desired_lo2_frequency, _mixer2_m, _mixer2_n);
+
+ // If the frequency is in a different tuning band, we need to switch filters
+ _rf_filter = _tune_settings.rf_fir;
+ _if1_filter = _tune_settings.if1_fir;
+ _if2_filter = _tune_settings.if2_fir;
+ _band_inverted =
+ _is_band_inverted(_trx, _desired_if2_frequency, _rfdc_rate, _tune_settings);
+}
+
+
+void zbx_freq_be_expert::resolve()
+{
+ if (_is_highband) {
+ _coerced_frequency =
+ ((_coerced_if2_frequency - (_coerced_lo2_frequency * _mixer2_n)) / _mixer2_m);
+ } else {
+ _coerced_frequency =
+ (_coerced_lo1_frequency
+ + ((_coerced_lo2_frequency * _mixer2_n - _coerced_if2_frequency)
+ / _mixer2_m))
+ / _mixer1_n;
+ }
+
+ // Users may change individual settings (LO frequencies, if2 frequencies) and throw
+ // the output frequency out of range. We have to stop here so that the gain API
+ // doesn't panic (Clipping here would have no effect on the actual output signal)
+ using namespace uhd::math::fp_compare;
+ if (fp_compare_delta<double>(_coerced_frequency.get()) < ZBX_MIN_FREQ
+ || fp_compare_delta<double>(_coerced_frequency.get()) > ZBX_MAX_FREQ) {
+ UHD_LOG_WARNING(get_name(),
+ "Resulting coerced frequency " << _coerced_frequency.get()
+ << " is out of range!");
+ }
+}
+
+void zbx_lo_expert::resolve()
+{
+ if (_test_mode_enabled.is_dirty()) {
+ _lo_ctrl->set_lo_test_mode_enabled(_test_mode_enabled);
+ }
+
+ if (_set_is_enabled.is_dirty()) {
+ _lo_ctrl->set_lo_port_enabled(_set_is_enabled);
+ }
+
+ if (_set_is_enabled && _desired_lo_frequency.is_dirty()) {
+ const double clipped_lo_freq = std::max(
+ LMX2572_MIN_FREQ, std::min(_desired_lo_frequency.get(), LMX2572_MAX_FREQ));
+ _coerced_lo_frequency = _lo_ctrl->set_lo_freq(clipped_lo_freq);
+ UHD_LOG_TRACE(get_name(),
+ "Requested " << _get_trx_string(_trx) << _chan << " frequency "
+ << (_desired_lo_frequency / 1e6) << "MHz was coerced to "
+ << (_coerced_lo_frequency / 1e6) << "MHz");
+ }
+}
+
+void zbx_gain_coercer_expert::resolve()
+{
+ _gain_coerced = _valid_range.clip(_gain_desired, true);
+}
+
+void zbx_tx_gain_expert::resolve()
+{
+ if (_profile != ZBX_GAIN_PROFILE_DEFAULT) {
+ return;
+ }
+
+ // If a user passes in a gain value, we have to set the Power API tracking mode
+ if (_gain_in.is_dirty()) {
+ _power_mgr->set_tracking_mode(uhd::usrp::pwr_cal_mgr::tracking_mode::TRACK_GAIN);
+ }
+
+ // Now we do the overall gain setting
+ // Look up DSA values by gain
+ _gain_out = ZBX_TX_GAIN_RANGE.clip(_gain_in, true);
+ const size_t gain_idx = _gain_out / TX_GAIN_STEP;
+ // Clip _frequency to valid ZBX range to avoid errors in the scenario when user
+ // manually configures LO frequencies and causes an illegal overall frequency
+ auto dsa_settings =
+ _dsa_cal->get_dsa_setting(ZBX_FREQ_RANGE.clip(_frequency), gain_idx);
+ // Now write to downstream nodes, converting attenuations to gains:
+ _dsa1 = static_cast<double>(ZBX_TX_DSA_MAX_ATT - dsa_settings[0]);
+ _dsa2 = static_cast<double>(ZBX_TX_DSA_MAX_ATT - dsa_settings[1]);
+ // Convert amp index to gain
+ _amp_gain = ZBX_TX_AMP_GAIN_MAP.at(static_cast<tx_amp>(dsa_settings[2]));
+}
+
+void zbx_rx_gain_expert::resolve()
+{
+ if (_profile != ZBX_GAIN_PROFILE_DEFAULT) {
+ return;
+ }
+
+ // If a user passes in a gain value, we have to set the Power API tracking mode
+ if (_gain_in.is_dirty()) {
+ _power_mgr->set_tracking_mode(uhd::usrp::pwr_cal_mgr::tracking_mode::TRACK_GAIN);
+ }
+
+ // Now we do the overall gain setting
+ if (_frequency.get() <= RX_LOW_FREQ_MAX_GAIN_CUTOFF) {
+ _gain_out = ZBX_RX_LOW_FREQ_GAIN_RANGE.clip(_gain_in, true);
+ } else {
+ _gain_out = ZBX_RX_GAIN_RANGE.clip(_gain_in, true);
+ }
+ // Now we do the overall gain setting
+ // Look up DSA values by gain
+ const size_t gain_idx = _gain_out / RX_GAIN_STEP;
+ // Clip _frequency to valid ZBX range to avoid errors in the scenario when user
+ // manually configures LO frequencies and causes an illegal overall frequency
+ auto dsa_settings =
+ _dsa_cal->get_dsa_setting(ZBX_FREQ_RANGE.clip(_frequency), gain_idx);
+ // Now write to downstream nodes, converting attenuation to gains:
+ _dsa1 = ZBX_RX_DSA_MAX_ATT - dsa_settings[0];
+ _dsa2 = ZBX_RX_DSA_MAX_ATT - dsa_settings[1];
+ _dsa3a = ZBX_RX_DSA_MAX_ATT - dsa_settings[2];
+ _dsa3b = ZBX_RX_DSA_MAX_ATT - dsa_settings[3];
+}
+
+void zbx_tx_programming_expert::resolve()
+{
+ if (_profile.is_dirty()) {
+ if (_profile == ZBX_GAIN_PROFILE_DEFAULT || _profile == ZBX_GAIN_PROFILE_MANUAL
+ || _profile == ZBX_GAIN_PROFILE_CPLD) {
+ _cpld->set_atr_mode(_chan,
+ zbx_cpld_ctrl::atr_mode_target::DSA,
+ zbx_cpld_ctrl::atr_mode::CLASSIC_ATR);
+ } else {
+ _cpld->set_atr_mode(_chan,
+ zbx_cpld_ctrl::atr_mode_target::DSA,
+ zbx_cpld_ctrl::atr_mode::SW_DEFINED);
+ }
+ }
+
+ // If we're in any of the table modes, then we don't write DSA and amp values
+ // A note on caching: The CPLD object caches state, and only pokes the CPLD
+ // if it's changed. However, all DSAs are on the same register. That means
+ // the DSA register changes, all DSA values written to the CPLD will come
+ // from the input data nodes to this worker node. This can overwrite DSA
+ // values if the cached version and the actual value on the CPLD differ.
+ if (_profile == ZBX_GAIN_PROFILE_DEFAULT || _profile == ZBX_GAIN_PROFILE_MANUAL) {
+ // Convert gains back to attenuation
+ zbx_cpld_ctrl::tx_dsa_type dsa_settings = {
+ uhd::narrow_cast<uint32_t>(ZBX_TX_DSA_MAX_ATT - _dsa1.get()),
+ uhd::narrow_cast<uint32_t>(ZBX_TX_DSA_MAX_ATT - _dsa2.get())};
+ _cpld->set_tx_gain_switches(_chan, ATR_ADDR_TX, dsa_settings);
+ _cpld->set_tx_gain_switches(_chan, ATR_ADDR_XX, dsa_settings);
+ }
+
+ // If frequency changed, we might have changed bands and the CPLD dsa tables need to
+ // be reloaded
+ // TODO: This is a major hack, and these tables should be loaded outside of the
+ // tuning call. This means every tuning request involves a large amount of CPLD
+ // writes.
+ // We only write when we aren't using a command time, otherwise all those CPLD
+ // commands will line up in the CPLD command queue, and diminish any purpose
+ // of timed commands in the first place
+ // Clip _frequency to valid ZBX range to avoid errors in the scenario when user
+ // manually configures LO frequencies and causes an illegal overall frequency
+ if (_command_time == 0.0) {
+ _cpld->update_tx_dsa_settings(
+ _dsa_cal->get_band_settings(ZBX_FREQ_RANGE.clip(_frequency), 0 /*dsa1*/),
+ _dsa_cal->get_band_settings(ZBX_FREQ_RANGE.clip(_frequency), 1 /*dsa2*/));
+ }
+
+ for (const size_t idx : ATR_ADDRS) {
+ _cpld->set_lo_source(idx,
+ zbx_lo_ctrl::lo_string_to_enum(TX_DIRECTION, _chan, ZBX_LO1),
+ _lo1_source);
+ _cpld->set_lo_source(idx,
+ zbx_lo_ctrl::lo_string_to_enum(TX_DIRECTION, _chan, ZBX_LO2),
+ _lo2_source);
+
+ _cpld->set_tx_rf_filter(_chan, idx, _rf_filter);
+ _cpld->set_tx_if1_filter(_chan, idx, _if1_filter);
+ _cpld->set_tx_if2_filter(_chan, idx, _if2_filter);
+ }
+
+ // Convert amp gain to amp index
+ UHD_ASSERT_THROW(ZBX_TX_GAIN_AMP_MAP.count(_amp_gain.get()));
+ const tx_amp amp = ZBX_TX_GAIN_AMP_MAP.at(_amp_gain.get());
+ _cpld->set_tx_antenna_switches(_chan, ATR_ADDR_0X, _antenna, tx_amp::BYPASS);
+ _cpld->set_tx_antenna_switches(_chan, ATR_ADDR_RX, _antenna, tx_amp::BYPASS);
+ _cpld->set_tx_antenna_switches(_chan, ATR_ADDR_TX, _antenna, amp);
+ _cpld->set_tx_antenna_switches(_chan, ATR_ADDR_XX, _antenna, amp);
+
+ // We do not update LEDs on switching TX antenna value by definition
+}
+
+void zbx_rx_programming_expert::resolve()
+{
+ if (_profile.is_dirty()) {
+ if (_profile == ZBX_GAIN_PROFILE_DEFAULT || _profile == ZBX_GAIN_PROFILE_MANUAL
+ || _profile == ZBX_GAIN_PROFILE_CPLD) {
+ _cpld->set_atr_mode(_chan,
+ zbx_cpld_ctrl::atr_mode_target::DSA,
+ zbx_cpld_ctrl::atr_mode::CLASSIC_ATR);
+ } else {
+ _cpld->set_atr_mode(_chan,
+ zbx_cpld_ctrl::atr_mode_target::DSA,
+ zbx_cpld_ctrl::atr_mode::SW_DEFINED);
+ }
+ }
+
+ // If we're in any of the table modes, then we don't write DSA values
+ // A note on caching: The CPLD object caches state, and only pokes the CPLD
+ // if it's changed. However, all DSAs are on the same register. That means
+ // the DSA register changes, all DSA values written to the CPLD will come
+ // from the input data nodes to this worker node. This can overwrite DSA
+ // values if the cached version and the actual value on the CPLD differ.
+ if (_profile == ZBX_GAIN_PROFILE_DEFAULT || _profile == ZBX_GAIN_PROFILE_MANUAL) {
+ zbx_cpld_ctrl::rx_dsa_type dsa_settings = {
+ uhd::narrow_cast<uint32_t>(ZBX_RX_DSA_MAX_ATT - _dsa1.get()),
+ uhd::narrow_cast<uint32_t>(ZBX_RX_DSA_MAX_ATT - _dsa2.get()),
+ uhd::narrow_cast<uint32_t>(ZBX_RX_DSA_MAX_ATT - _dsa3a.get()),
+ uhd::narrow_cast<uint32_t>(ZBX_RX_DSA_MAX_ATT - _dsa3b.get())};
+ _cpld->set_rx_gain_switches(_chan, ATR_ADDR_RX, dsa_settings);
+ _cpld->set_rx_gain_switches(_chan, ATR_ADDR_XX, dsa_settings);
+ }
+
+
+ // If frequency changed, we might have changed bands and the CPLD dsa tables need to
+ // be reloaded
+ // TODO: This is a major hack, and these tables should be loaded outside of the
+ // tuning call. This means every tuning request involves a large amount of CPLD
+ // writes.
+ // We only write when we aren't using a command time, otherwise all those CPLD
+ // commands will line up in the CPLD command queue, and diminish any purpose
+ // of timed commands in the first place
+ // Clip _frequency to valid ZBX range to avoid errors in the scenario when user
+ // manually configures LO frequencies and causes an illegal overall frequency
+ if (_command_time == 0.0) {
+ _cpld->update_rx_dsa_settings(
+ _dsa_cal->get_band_settings(ZBX_FREQ_RANGE.clip(_frequency), 0 /*dsa1*/),
+ _dsa_cal->get_band_settings(ZBX_FREQ_RANGE.clip(_frequency), 1 /*dsa2*/),
+ _dsa_cal->get_band_settings(ZBX_FREQ_RANGE.clip(_frequency), 2 /*dsa3a*/),
+ _dsa_cal->get_band_settings(ZBX_FREQ_RANGE.clip(_frequency), 3 /*dsa3b*/));
+ }
+
+ for (const size_t idx : ATR_ADDRS) {
+ _cpld->set_lo_source(idx,
+ zbx_lo_ctrl::lo_string_to_enum(RX_DIRECTION, _chan, ZBX_LO1),
+ _lo1_source);
+ _cpld->set_lo_source(idx,
+ zbx_lo_ctrl::lo_string_to_enum(RX_DIRECTION, _chan, ZBX_LO2),
+ _lo2_source);
+
+ // If using the TX/RX terminal, only configure the ATR RX state since the
+ // state of the switch at other times is controlled by TX
+ if (_antenna != ANTENNA_TXRX || idx == ATR_ADDR_RX) {
+ _cpld->set_rx_antenna_switches(_chan, idx, _antenna);
+ }
+
+ _cpld->set_rx_rf_filter(_chan, idx, _rf_filter);
+ _cpld->set_rx_if1_filter(_chan, idx, _if1_filter);
+ _cpld->set_rx_if2_filter(_chan, idx, _if2_filter);
+ }
+
+ _update_leds();
+}
+
+void zbx_rx_programming_expert::_update_leds()
+{
+ if (_atr_mode != zbx_cpld_ctrl::atr_mode::CLASSIC_ATR) {
+ return;
+ }
+ // We default to the RX1 LED for all RX antenna values that are not TX/RX0
+ const bool rx_on_trx = _antenna == ANTENNA_TXRX;
+ // clang-format off
+ // G==Green, R==Red RX2 TX/RX-G TX/RX-R
+ _cpld->set_leds(_chan, ATR_ADDR_0X, false, false, false);
+ _cpld->set_leds(_chan, ATR_ADDR_RX, !rx_on_trx, rx_on_trx, false);
+ _cpld->set_leds(_chan, ATR_ADDR_TX, false, false, true );
+ _cpld->set_leds(_chan, ATR_ADDR_XX, !rx_on_trx, rx_on_trx, true );
+ // clang-format on
+}
+
+void zbx_band_inversion_expert::resolve()
+{
+ _rpcc->enable_iq_swap(_is_band_inverted.get(), _get_trx_string(_trx), _chan);
+}
+
+void zbx_rfdc_freq_expert::resolve()
+{
+ // Because we can configure both IF2 and the RFDC NCO frequency, these may
+ // come into conflict. We choose IF2 over RFDC in that case. In other words
+ // the only time we choose the desired RFDC frequency over the IF2 (when in
+ // conflict) is when the RFDC freq was changed directly.
+ const double desired_rfdc_freq = [&]() -> double {
+ if (_rfdc_freq_desired.is_dirty() && !_if2_frequency_desired.is_dirty()) {
+ return _rfdc_freq_desired;
+ }
+ return _if2_frequency_desired;
+ }();
+
+ _rfdc_freq_coerced = _rpcc->rfdc_set_nco_freq(
+ _get_trx_string(_trx), _db_idx, _chan, desired_rfdc_freq);
+ _if2_frequency_coerced = _rfdc_freq_coerced;
+}
+
+void zbx_sync_expert::resolve()
+{
+ // Some local helper consts
+ // clang-format off
+ constexpr std::array<std::array<zbx_lo_t, 4>, 2> los{{{
+ zbx_lo_t::RX0_LO1,
+ zbx_lo_t::RX0_LO2,
+ zbx_lo_t::TX0_LO1,
+ zbx_lo_t::TX0_LO2
+ }, {
+ zbx_lo_t::RX1_LO1,
+ zbx_lo_t::RX1_LO2,
+ zbx_lo_t::TX1_LO1,
+ zbx_lo_t::TX1_LO2
+ }}};
+ constexpr std::array<std::array<rfdc_control::rfdc_type, 2>, 2> ncos{{
+ {rfdc_control::rfdc_type::RX0, rfdc_control::rfdc_type::TX0},
+ {rfdc_control::rfdc_type::RX1, rfdc_control::rfdc_type::TX1}
+ }};
+ // clang-format on
+
+ // Now do some timing checks
+ const std::vector<bool> chan_needs_sync = {_fe_time.at(0) != uhd::time_spec_t::ASAP,
+ _fe_time.at(1) != uhd::time_spec_t::ASAP};
+ // If there's no command time, no need to synchronize anything
+ if (!chan_needs_sync[0] && !chan_needs_sync[1]) {
+ UHD_LOG_TRACE(get_name(), "No command time: Skipping phase sync.");
+ return;
+ }
+ const bool times_match = _fe_time.at(0) == _fe_time.at(1);
+
+ // ** Find LOs to synchronize *********************************************
+ // Find dirty LOs which need sync'ing
+ std::set<zbx_lo_t> los_to_sync;
+ for (const size_t chan : ZBX_CHANNELS) {
+ if (chan_needs_sync[chan]) {
+ for (const auto& lo_idx : los[chan]) {
+ if (_lo_freqs.at(lo_idx).is_dirty()) {
+ los_to_sync.insert(lo_idx);
+ }
+ }
+ }
+ }
+
+ // ** Find NCOs to synchronize ********************************************
+ // Same rules apply as for LOs.
+ std::set<rfdc_control::rfdc_type> ncos_to_sync;
+ for (const size_t chan : ZBX_CHANNELS) {
+ if (chan_needs_sync[chan]) {
+ for (const auto& nco_idx : ncos[chan]) {
+ if (_nco_freqs.at(nco_idx).is_dirty()) {
+ ncos_to_sync.insert(nco_idx);
+ }
+ }
+ }
+ }
+
+ // ** Find ADC/DAC gearboxes to synchronize *******************************
+ // Gearboxes are special, because they only need to be synchronized once
+ // per session, assuming the command time has been set. Unfortunately we
+ // have no way here to know if the timekeeper time was updated, but it is
+ // well documented that in order to synchronize devices, one first has to
+ // make sure the timekeepers are running in sync (by calling
+ // set_time_next_pps() accordingly).
+ // The logic we use here is that we will always have to update the NCO when
+ // doing a synced tune, so we update all the gearboxes for the NCOs -- but
+ // only if they have not yet been synchronized.
+ std::set<rfdc_control::rfdc_type> gearboxes_to_sync;
+ if (!_adcs_synced) {
+ for (const auto rfdc :
+ {rfdc_control::rfdc_type::RX0, rfdc_control::rfdc_type::RX1}) {
+ if (ncos_to_sync.count(rfdc)) {
+ gearboxes_to_sync.insert(rfdc);
+ // Technically, they're not synced yet but this saves us from
+ // having to look up which RFDCs map to RX again later
+ _adcs_synced = true;
+ }
+ }
+ }
+ if (!_dacs_synced) {
+ for (const auto rfdc :
+ {rfdc_control::rfdc_type::TX0, rfdc_control::rfdc_type::TX1}) {
+ if (ncos_to_sync.count(rfdc)) {
+ gearboxes_to_sync.insert(rfdc);
+ // Technically, they're not synced yet but this saves us from
+ // having to look up which RFDCs map to TX again later
+ _dacs_synced = true;
+ }
+ }
+ }
+
+ // ** Do synchronization **************************************************
+ // This is where we orchestrate the sync commands. If sync commands happen
+ // at different times, we make sure to send out the earlier one first.
+ // If we need to schedule things a bit differently, e.g., we need to
+ // manually calculate offsets from the command time so that LO and NCO sync
+ // pulses line up, it most likely makes sense to use the scheduling expert
+ // for that, and calculate different times for different events there.
+ if (times_match) {
+ UHD_LOG_TRACE(get_name(),
+ "Syncing all channels: " << los_to_sync.size() << " LO(s), "
+ << ncos_to_sync.size() << " NCO(s), and "
+ << gearboxes_to_sync.size() << " gearbox(es).")
+ if (!gearboxes_to_sync.empty()) {
+ _rfdcc->reset_gearboxes(
+ std::vector<rfdc_control::rfdc_type>(
+ gearboxes_to_sync.cbegin(), gearboxes_to_sync.cend()),
+ _fe_time.at(0).get());
+ }
+ if (!los_to_sync.empty()) {
+ _cpld->pulse_lo_sync(
+ 0, std::vector<zbx_lo_t>(los_to_sync.cbegin(), los_to_sync.cend()));
+ }
+ if (!ncos_to_sync.empty()) {
+ _rfdcc->reset_ncos(std::vector<rfdc_control::rfdc_type>(
+ ncos_to_sync.cbegin(), ncos_to_sync.cend()),
+ _fe_time.at(0).get());
+ }
+ } else {
+ // If the command times differ, we need to manually reorder the commands
+ // such that the channel with the earlier time gets precedence
+ const size_t first_sync_chan =
+ (times_match || (_fe_time.at(0) <= _fe_time.at(1))) ? 0 : 1;
+ const auto sync_order = (first_sync_chan == 0) ? std::vector<size_t>{0, 1}
+ : std::vector<size_t>{1, 0};
+ for (const size_t chan : sync_order) {
+ std::vector<zbx_lo_t> this_chan_los;
+ for (const zbx_lo_t lo_idx : los[chan]) {
+ if (los_to_sync.count(lo_idx)) {
+ this_chan_los.push_back(lo_idx);
+ }
+ }
+
+ std::vector<rfdc_control::rfdc_type> this_chan_ncos;
+ for (const auto nco_idx : ncos[chan]) {
+ if (ncos_to_sync.count(nco_idx)) {
+ this_chan_ncos.push_back(nco_idx);
+ }
+ }
+ std::vector<rfdc_control::rfdc_type> this_chan_gearboxes;
+ for (const auto gb_idx : ncos[chan]) {
+ if (gearboxes_to_sync.count(gb_idx)) {
+ this_chan_gearboxes.push_back(gb_idx);
+ }
+ }
+ UHD_LOG_TRACE(get_name(),
+ "Syncing channel " << chan << ": " << this_chan_los.size()
+ << " LO(s) and " << this_chan_ncos.size()
+ << " NCO(s).");
+ if (!this_chan_gearboxes.empty()) {
+ UHD_LOG_TRACE(get_name(),
+ "Resetting " << this_chan_gearboxes.size() << " gearboxes.");
+ _rfdcc->reset_gearboxes(this_chan_gearboxes, _fe_time.at(chan).get());
+ }
+ if (!this_chan_los.empty()) {
+ _cpld->pulse_lo_sync(chan, this_chan_los);
+ }
+ if (!this_chan_ncos.empty()) {
+ _rfdcc->reset_ncos(this_chan_ncos, _fe_time.at(chan).get());
+ }
+ }
+ }
+} // zbx_sync_expert::resolve()
+
+// End expert resolve sections
+
+}}} // namespace uhd::usrp::zbx
diff --git a/host/lib/usrp/dboard/zbx/zbx_lo_ctrl.cpp b/host/lib/usrp/dboard/zbx/zbx_lo_ctrl.cpp
new file mode 100644
index 000000000..1af665207
--- /dev/null
+++ b/host/lib/usrp/dboard/zbx/zbx_lo_ctrl.cpp
@@ -0,0 +1,162 @@
+//
+// Copyright 2020 Ettus Research, a National Instruments Brand
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+#include <uhd/exception.hpp>
+#include <uhd/utils/log.hpp>
+#include <uhdlib/usrp/dboard/zbx/zbx_lo_ctrl.hpp>
+#include <thread>
+
+namespace uhd { namespace usrp { namespace zbx {
+
+zbx_lo_ctrl::zbx_lo_ctrl(zbx_lo_t lo,
+ lmx2572_iface::write_fn_t&& poke16,
+ lmx2572_iface::read_fn_t&& peek16,
+ lmx2572_iface::sleep_fn_t&& sleep,
+ const double default_frequency,
+ const double db_prc_rate,
+ const bool testing_mode_enabled)
+ : _lo(lo)
+ , _log_id(ZBX_LO_LOG_ID.at(lo))
+ , _freq(default_frequency)
+ , _db_prc_rate(db_prc_rate)
+ , _testing_mode_enabled(testing_mode_enabled)
+{
+ _lmx = lmx2572_iface::make(std::move(poke16), std::move(peek16), std::move(sleep));
+ UHD_ASSERT_THROW(_lmx);
+ UHD_LOG_TRACE(_log_id, "LO initialized...");
+ _lmx->reset();
+
+ set_lo_port_enabled(true);
+ // In ZBX, we always run the LOs in sync mode. It is theoretically possible
+ // to not do so, but we gain nothing by doing that.
+ _lmx->set_sync_mode(true);
+ set_lo_freq(LMX2572_DEFAULT_FREQ);
+ wait_for_lo_lock();
+}
+
+double zbx_lo_ctrl::set_lo_freq(const double freq)
+{
+ UHD_ASSERT_THROW(_lmx);
+ UHD_LOG_TRACE(_log_id, "Setting LO frequency " << freq / 1e6 << " MHz");
+
+ _freq = _lmx->set_frequency(freq,
+ _db_prc_rate,
+ false /*TODO: get_spur_dodging()*/);
+ _lmx->commit();
+ return _freq;
+}
+
+double zbx_lo_ctrl::get_lo_freq()
+{
+ return _freq;
+}
+
+void zbx_lo_ctrl::wait_for_lo_lock()
+{
+ UHD_LOG_TRACE(_log_id, "Waiting for LO lock, " << ZBX_LO_LOCK_TIMEOUT_MS << " ms");
+ const auto timeout = std::chrono::steady_clock::now()
+ + std::chrono::milliseconds(ZBX_LO_LOCK_TIMEOUT_MS);
+ while (std::chrono::steady_clock::now() < timeout && !get_lock_status()) {
+ std::this_thread::sleep_for(std::chrono::milliseconds(1));
+ }
+ if (!get_lock_status()) {
+ // If we can't lock our LO, this could be a lot of possible issues
+ throw uhd::runtime_error(_log_id + " has failed to lock!");
+ }
+}
+
+bool zbx_lo_ctrl::get_lock_status()
+{
+ return _lmx->get_lock_status();
+}
+
+void zbx_lo_ctrl::set_lo_port_enabled(bool enable)
+{
+ UHD_LOG_TRACE(_log_id,
+ "Enabling LO " << (_testing_mode_enabled ? "test" : "output") << " port");
+
+ // We want to set the output port regardless of test mode being enabled
+ _lmx->set_output_enable(_get_output_port(false), enable);
+
+ if (_testing_mode_enabled && enable) {
+ // If testing mode is enabled, also set the test port
+ _lmx->set_output_enable(_get_output_port(true), true);
+ } else {
+ // If testing mode is disabled, test port should be disabled
+ _lmx->set_output_enable(_get_output_port(true), false);
+ _lmx->set_mux_input(
+ _get_output_port(true), lmx2572_iface::mux_in_t::HIGH_IMPEDANCE);
+ }
+
+ _lmx->set_enabled(enable);
+ _lmx->commit();
+}
+
+bool zbx_lo_ctrl::get_lo_port_enabled()
+{
+ return _lmx->get_enabled();
+}
+
+void zbx_lo_ctrl::set_lo_test_mode_enabled(bool enable)
+{
+ _testing_mode_enabled = enable;
+ set_lo_port_enabled(get_lo_port_enabled());
+}
+
+bool zbx_lo_ctrl::get_lo_test_mode_enabled()
+{
+ return _testing_mode_enabled;
+}
+
+zbx_lo_t zbx_lo_ctrl::lo_string_to_enum(
+ const uhd::direction_t trx, const size_t channel, const std::string name)
+{
+ if (trx == TX_DIRECTION) {
+ if (channel == 0) {
+ if (name == ZBX_LO1) {
+ return zbx_lo_t::TX0_LO1;
+ } else if (name == ZBX_LO2) {
+ return zbx_lo_t::TX0_LO2;
+ }
+ } else if (channel == 1) {
+ if (name == ZBX_LO1) {
+ return zbx_lo_t::TX1_LO1;
+ } else if (name == ZBX_LO2) {
+ return zbx_lo_t::TX1_LO2;
+ }
+ }
+ } else {
+ if (channel == 0) {
+ if (name == ZBX_LO1) {
+ return zbx_lo_t::RX0_LO1;
+ } else if (name == ZBX_LO2) {
+ return zbx_lo_t::RX0_LO2;
+ }
+ } else if (channel == 1) {
+ if (name == ZBX_LO1) {
+ return zbx_lo_t::RX1_LO1;
+ } else if (name == ZBX_LO2) {
+ return zbx_lo_t::RX1_LO2;
+ }
+ }
+ }
+ UHD_THROW_INVALID_CODE_PATH();
+}
+
+lmx2572_iface::output_t zbx_lo_ctrl::_get_output_port(bool testing_mode)
+{
+ // Note: The LO output ports here are dependent to the LO and zbx hardware
+ // configuration, in no particular order (zbx radio configuration output vs.
+ // test port output)
+ if (!testing_mode) {
+ // Rev B has all LO outputs on Port A
+ return lmx2572_iface::output_t::RF_OUTPUT_A;
+ } else {
+ return lmx2572_iface::output_t::RF_OUTPUT_B;
+ }
+}
+
+}}} // namespace uhd::usrp::zbx
diff --git a/host/lib/usrp/mpmd/mpmd_devices.hpp b/host/lib/usrp/mpmd/mpmd_devices.hpp
index 9cc046037..689e5c981 100644
--- a/host/lib/usrp/mpmd/mpmd_devices.hpp
+++ b/host/lib/usrp/mpmd/mpmd_devices.hpp
@@ -19,6 +19,6 @@ static const std::vector<std::string> MPM_DEVICE_TYPES = {
MPM_CATCHALL_DEVICE_TYPE,
"n3xx",
"e3xx",
-};
+ "x4xx"};
#endif /* INCLUDED_MPMD_DEVICES_HPP */
diff --git a/host/lib/usrp/mpmd/mpmd_image_loader.cpp b/host/lib/usrp/mpmd/mpmd_image_loader.cpp
index 145ad70c9..c15c4832c 100644
--- a/host/lib/usrp/mpmd/mpmd_image_loader.cpp
+++ b/host/lib/usrp/mpmd/mpmd_image_loader.cpp
@@ -10,10 +10,13 @@
#include <uhd/device.hpp>
#include <uhd/exception.hpp>
#include <uhd/image_loader.hpp>
+#include <uhd/rfnoc/radio_control.hpp>
+#include <uhd/rfnoc_graph.hpp>
#include <uhd/types/component_file.hpp>
#include <uhd/types/eeprom.hpp>
#include <uhd/utils/paths.hpp>
#include <uhd/utils/static.hpp>
+#include <uhdlib/features/fpga_load_notification_iface.hpp>
#include <uhdlib/utils/prefs.hpp>
#include <boost/algorithm/string.hpp>
#include <boost/archive/iterators/binary_from_base64.hpp>
@@ -368,6 +371,19 @@ static bool mpmd_image_loader(const image_loader::image_loader_args_t& image_loa
mpmd_send_fpga_to_device(image_loader_args, dev_addr);
+ {
+ // All MPM devices use RFNoC
+ auto graph = rfnoc::rfnoc_graph::make(find_hint);
+ for (size_t mb_index = 0; mb_index < graph->get_num_mboards(); mb_index++) {
+ auto mboard = graph->get_mb_controller(mb_index);
+ if (mboard->has_feature<uhd::features::fpga_load_notification_iface>()) {
+ auto& notifier =
+ mboard->get_feature<uhd::features::fpga_load_notification_iface>();
+ notifier.onload();
+ }
+ }
+ }
+
return true;
}
@@ -385,4 +401,5 @@ UHD_STATIC_BLOCK(register_mpm_image_loader)
// time being
image_loader::register_image_loader("n3xx", mpmd_image_loader, recovery_instructions);
image_loader::register_image_loader("e3xx", mpmd_image_loader, recovery_instructions);
+ image_loader::register_image_loader("x4xx", mpmd_image_loader, recovery_instructions);
}
diff --git a/host/lib/usrp/mpmd/mpmd_mb_controller.cpp b/host/lib/usrp/mpmd/mpmd_mb_controller.cpp
index dba8713c4..9bbd324b0 100644
--- a/host/lib/usrp/mpmd/mpmd_mb_controller.cpp
+++ b/host/lib/usrp/mpmd/mpmd_mb_controller.cpp
@@ -14,6 +14,44 @@ namespace {
constexpr size_t MPMD_DEFAULT_LONG_TIMEOUT = 30000; // ms
} // namespace
+mpmd_mb_controller::fpga_onload::fpga_onload()
+{}
+
+void mpmd_mb_controller::fpga_onload::onload()
+{
+ for (auto& cb : _cbs)
+ {
+ if (auto spt = cb.lock())
+ {
+ spt->onload();
+ }
+ }
+}
+
+void mpmd_mb_controller::fpga_onload::request_cb(uhd::features::fpga_load_notification_iface::sptr handler)
+{
+ _cbs.emplace_back(handler);
+}
+
+mpmd_mb_controller::ref_clk_calibration::ref_clk_calibration(uhd::usrp::mpmd_rpc_iface::sptr rpcc)
+ : _rpcc(rpcc)
+{}
+
+void mpmd_mb_controller::ref_clk_calibration::set_ref_clk_tuning_word(uint32_t tuning_word)
+{
+ _rpcc->set_ref_clk_tuning_word(tuning_word);
+}
+
+uint32_t mpmd_mb_controller::ref_clk_calibration::get_ref_clk_tuning_word()
+{
+ return _rpcc->get_ref_clk_tuning_word();
+}
+
+void mpmd_mb_controller::ref_clk_calibration::store_ref_clk_tuning_word(uint32_t tuning_word)
+{
+ _rpcc->store_ref_clk_tuning_word(tuning_word);
+}
+
mpmd_mb_controller::mpmd_mb_controller(
uhd::usrp::mpmd_rpc_iface::sptr rpcc, uhd::device_addr_t device_info)
: _rpc(rpcc), _device_info(device_info)
@@ -33,6 +71,14 @@ mpmd_mb_controller::mpmd_mb_controller(
for (const auto& bank : _gpio_banks) {
_gpio_srcs.insert({bank, _rpc->get_gpio_srcs(bank)});
}
+
+ _fpga_onload = std::make_shared<fpga_onload>();
+ register_feature(_fpga_onload);
+
+ if (_rpc->supports_feature("ref_clk_calibration")) {
+ _ref_clk_cal = std::make_shared<ref_clk_calibration>(_rpc);
+ register_feature(_ref_clk_cal);
+ }
}
/******************************************************************************
@@ -142,16 +188,22 @@ std::vector<device_addr_t> mpmd_mb_controller::get_sync_sources()
return result;
}
-void mpmd_mb_controller::set_clock_source_out(const bool /*enb*/)
+void mpmd_mb_controller::set_clock_source_out(const bool enb)
{
- throw uhd::not_implemented_error(
- "set_clock_source_out() not implemented on this device!");
+ _rpc->set_clock_source_out(enb);
}
-void mpmd_mb_controller::set_time_source_out(const bool /*enb*/)
+void mpmd_mb_controller::set_time_source_out(const bool enb)
{
- throw uhd::not_implemented_error(
- "set_time_source_out() not implemented on this device!");
+ if (_rpc->supports_feature("time_export"))
+ {
+ _rpc->set_trigger_io(enb ? "pps_output" : "off");
+ }
+ else
+ {
+ throw uhd::not_implemented_error(
+ "set_time_source_out() not implemented on this device!");
+ }
}
sensor_value_t mpmd_mb_controller::get_sensor(const std::string& name)
diff --git a/host/lib/usrp/x300/x300_mb_controller.hpp b/host/lib/usrp/x300/x300_mb_controller.hpp
index 6cfa3f525..a0ffb0654 100644
--- a/host/lib/usrp/x300/x300_mb_controller.hpp
+++ b/host/lib/usrp/x300/x300_mb_controller.hpp
@@ -11,6 +11,7 @@
#include "x300_device_args.hpp"
#include "x300_radio_mbc_iface.hpp"
#include "x300_regs.hpp"
+#include <uhdlib/features/discoverable_feature_registry.hpp>
#include <uhd/rfnoc/mb_controller.hpp>
#include <uhd/types/sensors.hpp>
#include <uhd/types/wb_iface.hpp>
@@ -28,7 +29,8 @@ namespace uhd { namespace rfnoc {
* - Controlling all time- and clock-related settings
* - Initialize and hold the GPS control
*/
-class x300_mb_controller : public mb_controller
+class x300_mb_controller : public mb_controller,
+ public ::uhd::features::discoverable_feature_registry
{
public:
/**************************************************************************
diff --git a/host/lib/usrp/x400/CMakeLists.txt b/host/lib/usrp/x400/CMakeLists.txt
new file mode 100644
index 000000000..b3efe3b60
--- /dev/null
+++ b/host/lib/usrp/x400/CMakeLists.txt
@@ -0,0 +1,20 @@
+#
+# Copyright 2020 Ettus Research, a National Instruments Brand
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+
+########################################################################
+# This file included, use CMake directory variables
+########################################################################
+
+LIBUHD_APPEND_SOURCES(
+ ${CMAKE_CURRENT_SOURCE_DIR}/adc_self_calibration.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/x400_radio_control.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/x400_rfdc_control.cpp
+)
+
+if(ENABLE_DPDK)
+ include_directories(${DPDK_INCLUDE_DIRS})
+ add_definitions(-DHAVE_DPDK)
+endif(ENABLE_DPDK)
diff --git a/host/lib/usrp/x400/adc_self_calibration.cpp b/host/lib/usrp/x400/adc_self_calibration.cpp
new file mode 100644
index 000000000..7deba7725
--- /dev/null
+++ b/host/lib/usrp/x400/adc_self_calibration.cpp
@@ -0,0 +1,196 @@
+//
+// Copyright 2020 Ettus Research, a National Instruments Brand
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+#include "adc_self_calibration.hpp"
+#include <uhd/utils/log.hpp>
+#include <uhd/utils/scope_exit.hpp>
+#include <chrono>
+#include <thread>
+
+using namespace std::chrono_literals;
+
+namespace uhd { namespace features {
+
+adc_self_calibration::adc_self_calibration(uhd::usrp::x400_rpc_iface::sptr rpcc,
+ const std::string rpc_prefix,
+ const std::string unique_id,
+ size_t db_number,
+ uhd::usrp::x400::x400_dboard_iface::sptr daughterboard)
+ : _rpcc(rpcc)
+ , _rpc_prefix(rpc_prefix)
+ , _db_number(db_number)
+ , _daughterboard(daughterboard)
+ , _unique_id(unique_id)
+{
+}
+
+void adc_self_calibration::run(size_t chan)
+{
+ const auto tx_gain_profile =
+ _daughterboard->get_tx_gain_profile_api()->get_gain_profile(chan);
+ const auto rx_gain_profile =
+ _daughterboard->get_rx_gain_profile_api()->get_gain_profile(chan);
+ if (tx_gain_profile != "default" || rx_gain_profile != "default") {
+ throw uhd::runtime_error("Cannot run ADC self calibration when gain "
+ "profile for RX or TX is not 'default'.");
+ }
+
+ // The frequency that we need to feed into the ADC is, by decree,
+ // 13109 / 32768 times the ADC sample rate. (approx. 1.2GHz for a 3Gsps rate)
+ const double spll_freq = _rpcc->get_spll_freq();
+ const double cal_tone_freq = spll_freq * 13109.0 / 32768.0;
+
+ const auto cal_params = _daughterboard->get_adc_self_cal_params(cal_tone_freq);
+
+ // Switch to CAL_LOOPBACK and save the current antenna
+ const auto rx_antenna = _daughterboard->get_rx_antenna(chan);
+ const auto tx_antenna = _daughterboard->get_tx_antenna(chan);
+
+ auto reset_antennas = uhd::utils::scope_exit::make([&]() {
+ _daughterboard->set_rx_antenna(rx_antenna, chan);
+ _daughterboard->set_tx_antenna(tx_antenna, chan);
+
+ // Waiting here allows some CPLD registers to be set. It's not clear
+ // to me why we require this wait. See azdo #1473824
+ constexpr auto fudge_time = 100ms;
+ std::this_thread::sleep_for(fudge_time);
+ });
+
+ _daughterboard->set_rx_antenna("CAL_LOOPBACK", chan);
+ _daughterboard->set_tx_antenna("CAL_LOOPBACK", chan);
+
+ // Configure the output DAC mux to output 1/2 full scale
+ // set_dac_mux_data uses 16-bit values.
+ _rpcc->set_dac_mux_data(32768 / 2, 0);
+
+ const size_t motherboard_channel_number = _db_number * 2 + chan;
+ _rpcc->set_dac_mux_enable(motherboard_channel_number, 1);
+ auto disable_dac_mux = uhd::utils::scope_exit::make(
+ [&]() { _rpcc->set_dac_mux_enable(motherboard_channel_number, 0); });
+
+ // Save all of the LO frequencies & sources
+ const double original_rx_freq = _daughterboard->get_rx_frequency(chan);
+ std::map<std::string, std::tuple<std::string, double>> rx_lo_state;
+ for (auto rx_lo : _daughterboard->get_rx_lo_names(chan)) {
+ const std::string source(_daughterboard->get_rx_lo_source(rx_lo, chan));
+ const double freq = _daughterboard->get_rx_lo_freq(rx_lo, chan);
+ rx_lo_state.emplace(std::piecewise_construct,
+ std::forward_as_tuple(rx_lo),
+ std::forward_as_tuple(source, freq));
+ }
+ auto restore_rx_los = uhd::utils::scope_exit::make([&]() {
+ _daughterboard->set_rx_frequency(original_rx_freq, chan);
+ for (auto entry : rx_lo_state) {
+ auto& lo = std::get<0>(entry);
+ auto& state = std::get<1>(entry);
+
+ auto& source = std::get<0>(state);
+ const double freq = std::get<1>(state);
+ _daughterboard->set_rx_lo_source(source, lo, chan);
+ _daughterboard->set_rx_lo_freq(freq, lo, chan);
+ }
+ });
+
+ const double original_tx_freq = _daughterboard->get_tx_frequency(chan);
+ std::map<std::string, std::tuple<std::string, double>> tx_lo_state;
+ for (auto tx_lo : _daughterboard->get_tx_lo_names(chan)) {
+ const std::string source(_daughterboard->get_tx_lo_source(tx_lo, chan));
+ const double freq = _daughterboard->get_tx_lo_freq(tx_lo, chan);
+ tx_lo_state.emplace(std::piecewise_construct,
+ std::forward_as_tuple(tx_lo),
+ std::forward_as_tuple(source, freq));
+ }
+ auto restore_tx_los = uhd::utils::scope_exit::make([&]() {
+ _daughterboard->set_tx_frequency(original_tx_freq, chan);
+ for (auto entry : tx_lo_state) {
+ auto& lo = std::get<0>(entry);
+ auto& state = std::get<1>(entry);
+
+ auto& source = std::get<0>(state);
+ const double freq = std::get<1>(state);
+ _daughterboard->set_tx_lo_source(source, lo, chan);
+ _daughterboard->set_tx_lo_freq(freq, lo, chan);
+ }
+ });
+
+ _daughterboard->set_tx_frequency(cal_params.tx_freq, chan);
+ _daughterboard->set_rx_frequency(cal_params.rx_freq, chan);
+
+ // Set & restore the gain
+ const double tx_gain = _daughterboard->get_tx_gain(chan);
+ const double rx_gain = _daughterboard->get_rx_gain(chan);
+ auto restore_gains = uhd::utils::scope_exit::make([&]() {
+ _daughterboard->get_rx_gain_profile_api()->set_gain_profile("default", chan);
+ _daughterboard->get_tx_gain_profile_api()->set_gain_profile("default", chan);
+
+ _daughterboard->set_tx_gain(tx_gain, chan);
+ _daughterboard->set_rx_gain(rx_gain, chan);
+ });
+
+ // Set the threshold to detect half-scale
+ // The setup_threshold call uses 14-bit ADC values
+ constexpr int hysteresis_reset_time = 100;
+ constexpr int hysteresis_reset_threshold = 8000;
+ constexpr int hysteresis_set_threshold = 8192;
+ _rpcc->setup_threshold(_db_number,
+ chan,
+ 0,
+ "hysteresis",
+ hysteresis_reset_time,
+ hysteresis_reset_threshold,
+ hysteresis_set_threshold);
+ bool found_gain = false;
+ for (double i = cal_params.min_gain; i <= cal_params.max_gain; i += 1.0) {
+ _daughterboard->get_rx_gain_profile_api()->set_gain_profile("default", chan);
+ _daughterboard->get_tx_gain_profile_api()->set_gain_profile("default", chan);
+
+ _daughterboard->set_tx_gain(i, chan);
+ _daughterboard->set_rx_gain(i, chan);
+
+ // Set the daughterboard to use the duplex entry in the DSA table which was
+ // configured in the set_?x_gain call. (note that with a LabVIEW FPGA, we don't
+ // control the ATR lines, hence why we override them here)
+ _daughterboard->get_rx_gain_profile_api()->set_gain_profile("table_noatr", chan);
+ _daughterboard->get_tx_gain_profile_api()->set_gain_profile("table_noatr", chan);
+
+ _daughterboard->set_rx_gain(0b11, chan);
+ _daughterboard->set_tx_gain(0b11, chan);
+
+ // Wait for it to settle
+ constexpr auto settle_time = 10ms;
+ std::this_thread::sleep_for(settle_time);
+
+ try {
+ const bool threshold_status =
+ _rpcc->get_threshold_status(_db_number, chan, 0);
+ if (threshold_status) {
+ found_gain = true;
+ break;
+ }
+ } catch (uhd::runtime_error&) {
+ // Catch & eat this error, the user has a 5.0 FPGA and so can't auto-gain
+ return;
+ }
+ }
+
+ if (!found_gain) {
+ throw uhd::runtime_error(
+ "Could not find appropriate gain for performing ADC self cal");
+ }
+
+ // (if required) unfreeze calibration
+ const std::vector<int> current_frozen_state = _rpcc->get_cal_frozen(_db_number, chan);
+ _rpcc->set_cal_frozen(0, _db_number, chan);
+ auto refreeze_adcs = uhd::utils::scope_exit::make(
+ [&]() { _rpcc->set_cal_frozen(current_frozen_state[0], _db_number, chan); });
+
+ // Let the ADC calibrate
+ // 2000ms was found experimentally to be sufficient
+ constexpr auto calibration_time = 2000ms;
+ std::this_thread::sleep_for(calibration_time);
+}
+
+}} // namespace uhd::features
diff --git a/host/lib/usrp/x400/adc_self_calibration.hpp b/host/lib/usrp/x400/adc_self_calibration.hpp
new file mode 100644
index 000000000..474c14e07
--- /dev/null
+++ b/host/lib/usrp/x400/adc_self_calibration.hpp
@@ -0,0 +1,46 @@
+//
+// Copyright 2020 Ettus Research, a National Instruments Brand
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+#pragma once
+
+#include <uhd/features/adc_self_calibration_iface.hpp>
+#include <uhdlib/usrp/common/rpc.hpp>
+#include <uhdlib/usrp/dboard/x400_dboard_iface.hpp>
+#include <string>
+
+namespace uhd { namespace features {
+
+using namespace uhd::rfnoc;
+
+class adc_self_calibration : public adc_self_calibration_iface
+{
+public:
+ adc_self_calibration(uhd::usrp::x400_rpc_iface::sptr rpcc,
+ const std::string rpc_prefix,
+ const std::string unique_id,
+ size_t db_number,
+ uhd::usrp::x400::x400_dboard_iface::sptr daughterboard);
+
+ void run(const size_t channel) override;
+
+private:
+ //! Reference to the RPC client
+ uhd::usrp::x400_rpc_iface::sptr _rpcc;
+
+ const std::string _rpc_prefix;
+
+ const size_t _db_number;
+
+ uhd::usrp::x400::x400_dboard_iface::sptr _daughterboard;
+
+ const std::string _unique_id;
+ std::string get_unique_id() const
+ {
+ return _unique_id;
+ }
+};
+
+}} // namespace uhd::features
diff --git a/host/lib/usrp/x400/x400_radio_control.cpp b/host/lib/usrp/x400/x400_radio_control.cpp
new file mode 100644
index 000000000..281ae7916
--- /dev/null
+++ b/host/lib/usrp/x400/x400_radio_control.cpp
@@ -0,0 +1,752 @@
+//
+// Copyright 2020 Ettus Research, a National Instruments Brand
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+#include "x400_radio_control.hpp"
+#include <uhd/rfnoc/registry.hpp>
+#include <uhd/utils/log.hpp>
+#include <uhd/utils/math.hpp>
+#include <uhdlib/usrp/common/x400_rfdc_control.hpp>
+#include <uhdlib/usrp/dboard/debug_dboard.hpp>
+#include <uhdlib/usrp/dboard/null_dboard.hpp>
+#include <uhdlib/usrp/dboard/zbx/zbx_dboard.hpp>
+
+namespace uhd { namespace rfnoc {
+
+x400_radio_control_impl::fpga_onload::fpga_onload(size_t num_channels,
+ uhd::features::adc_self_calibration_iface::sptr adc_self_cal,
+ std::string unique_id)
+ : _num_channels(num_channels), _adc_self_cal(adc_self_cal), _unique_id(unique_id)
+{
+}
+
+void x400_radio_control_impl::fpga_onload::onload()
+{
+ for (size_t channel = 0; channel < _num_channels; channel++) {
+ if (_adc_self_cal) {
+ try {
+ _adc_self_cal->run(channel);
+ } catch (uhd::runtime_error& e) {
+ RFNOC_LOG_WARNING("Failure while running self cal on channel "
+ << channel << ": " << e.what());
+ }
+ }
+ }
+}
+
+x400_radio_control_impl::x400_radio_control_impl(make_args_ptr make_args)
+ : radio_control_impl(std::move(make_args))
+{
+ RFNOC_LOG_TRACE("Initializing x400_radio_control");
+
+ UHD_ASSERT_THROW(get_block_id().get_block_count() < 2);
+ constexpr char radio_slot_name[2] = {'A', 'B'};
+ _radio_slot = radio_slot_name[get_block_id().get_block_count()];
+ _rpc_prefix = get_block_id().get_block_count() == 1 ? "db_1_" : "db_0_";
+
+ UHD_ASSERT_THROW(get_mb_controller());
+ _mb_control = std::dynamic_pointer_cast<mpmd_mb_controller>(get_mb_controller());
+
+ _x4xx_timekeeper = std::dynamic_pointer_cast<mpmd_mb_controller::mpmd_timekeeper>(
+ _mb_control->get_timekeeper(0));
+ UHD_ASSERT_THROW(_x4xx_timekeeper);
+
+ _rpcc = _mb_control->dynamic_cast_rpc_as<uhd::usrp::x400_rpc_iface>();
+ if (!_rpcc) {
+ _rpcc = std::make_shared<uhd::usrp::x400_rpc>(_mb_control->get_rpc_client());
+ }
+
+ _db_rpcc = _mb_control->dynamic_cast_rpc_as<uhd::usrp::dboard_base_rpc_iface>();
+ if (!_db_rpcc) {
+ _db_rpcc = std::make_shared<uhd::usrp::dboard_base_rpc>(
+ _mb_control->get_rpc_client(), _rpc_prefix);
+ }
+
+ const auto all_dboard_info = _rpcc->get_dboard_info();
+ RFNOC_LOG_TRACE("Hardware detected " << all_dboard_info.size() << " daughterboards.");
+
+ // If we have two radio blocks, but there is only one dboard plugged in,
+ // we skip initialization. The board needs to be in slot A
+ if (all_dboard_info.size() <= get_block_id().get_block_count()) {
+ RFNOC_LOG_WARNING("The number of discovered daughterboards did not match the "
+ "number of radio blocks. Skipping front end initialization.");
+ _daughterboard = std::make_shared<null_dboard_impl>();
+ return;
+ }
+
+ const double master_clock_rate = _rpcc->get_master_clock_rate();
+ set_tick_rate(master_clock_rate);
+ _x4xx_timekeeper->update_tick_rate(master_clock_rate);
+ radio_control_impl::set_rate(master_clock_rate);
+
+ for (auto& samp_rate_prop : _samp_rate_in) {
+ set_property(samp_rate_prop.get_id(), get_rate(), samp_rate_prop.get_src_info());
+ }
+ for (auto& samp_rate_prop : _samp_rate_out) {
+ set_property(samp_rate_prop.get_id(), get_rate(), samp_rate_prop.get_src_info());
+ }
+
+ _validate_master_clock_rate_args();
+ _init_mpm();
+
+ RFNOC_LOG_TRACE("Initializing RFDC controls...");
+ _rfdcc = std::make_shared<x400::rfdc_control>(
+ // clang-format off
+ uhd::memmap32_iface_timed{
+ [this](const uint32_t addr, const uint32_t data, const uhd::time_spec_t& time_spec) {
+ regs().poke32(addr + x400_regs::RFDC_CTRL_BASE, data, time_spec);
+ },
+ [this](const uint32_t addr) {
+ return regs().peek32(addr + x400_regs::RFDC_CTRL_BASE);
+ }
+ },
+ // clang-format on
+ get_unique_id() + "::RFDC");
+
+ const auto& dboard = all_dboard_info[get_block_id().get_block_count()];
+ const std::string pid(dboard.at("pid").begin(), dboard.at("pid").end());
+ RFNOC_LOG_TRACE("Initializing daughterboard driver for PID " << pid);
+
+ // We may have physical daughterboards in the system, but no GPIO interface to the
+ // daughterboard in the FPGA. In this case, just instantiate the null daughterboard.
+ if (!_rpcc->is_db_gpio_ifc_present(get_block_id().get_block_count())) {
+ RFNOC_LOG_WARNING(
+ "Skipping daughterboard initialization, no GPIO interface in FPGA");
+ _daughterboard = std::make_shared<null_dboard_impl>();
+ return;
+ }
+
+ if (std::stol(pid) == uhd::usrp::zbx::ZBX_PID) {
+ auto zbx_rpc_sptr = _mb_control->dynamic_cast_rpc_as<uhd::usrp::zbx_rpc_iface>();
+ if (!zbx_rpc_sptr) {
+ zbx_rpc_sptr = std::make_shared<uhd::usrp::zbx_rpc>(
+ _mb_control->get_rpc_client(), _rpc_prefix);
+ }
+ _daughterboard = std::make_shared<uhd::usrp::zbx::zbx_dboard_impl>(
+ regs(),
+ regmap::PERIPH_BASE,
+ [this](const size_t instance) { return get_command_time(instance); },
+ get_block_id().get_block_count(),
+ _radio_slot,
+ _rpc_prefix,
+ get_unique_id(),
+ _rpcc,
+ zbx_rpc_sptr,
+ _rfdcc,
+ get_tree());
+ } else if (std::stol(pid) == uhd::rfnoc::DEBUG_DB_PID) {
+ _daughterboard = std::make_shared<debug_dboard_impl>();
+ } else if (std::stol(pid) == uhd::rfnoc::IF_TEST_DBOARD_PID) {
+ _daughterboard =
+ std::make_shared<if_test_dboard_impl>(get_block_id().get_block_count(),
+ _rpc_prefix,
+ get_unique_id(),
+ _mb_control,
+ get_tree());
+ } else if (std::stol(pid) == uhd::rfnoc::EMPTY_DB_PID) {
+ _daughterboard = std::make_shared<empty_slot_dboard_impl>();
+ set_num_output_ports(0);
+ set_num_input_ports(0);
+ } else {
+ RFNOC_LOG_WARNING("Skipping Daughterboard initialization for unsupported PID "
+ << "0x" << std::hex << std::stol(pid));
+ _daughterboard = std::make_shared<null_dboard_impl>();
+ return;
+ }
+
+ _init_prop_tree();
+
+ _rx_pwr_mgr = _daughterboard->get_pwr_mgr(uhd::RX_DIRECTION);
+ _tx_pwr_mgr = _daughterboard->get_pwr_mgr(uhd::TX_DIRECTION);
+
+ _tx_gain_profile_api = _daughterboard->get_tx_gain_profile_api();
+ _rx_gain_profile_api = _daughterboard->get_rx_gain_profile_api();
+
+ if (_daughterboard->is_adc_self_cal_supported()) {
+ _adc_self_calibration =
+ std::make_shared<uhd::features::adc_self_calibration>(_rpcc,
+ _rpc_prefix,
+ get_unique_id(),
+ get_block_id().get_block_count(),
+ _daughterboard);
+ register_feature(_adc_self_calibration);
+ }
+
+ _fpga_onload = std::make_shared<fpga_onload>(
+ get_num_output_ports(), _adc_self_calibration, get_unique_id());
+ register_feature(_fpga_onload);
+ _mb_control->_fpga_onload->request_cb(_fpga_onload);
+}
+
+void x400_radio_control_impl::_init_prop_tree()
+{
+ auto subtree = get_tree()->subtree(fs_path("mboard"));
+
+ for (size_t chan_idx = 0; chan_idx < get_num_output_ports(); chan_idx++) {
+ const fs_path rx_codec_path =
+ fs_path("rx_codec") / get_dboard_fe_from_chan(chan_idx, uhd::RX_DIRECTION);
+ const fs_path tx_codec_path =
+ fs_path("tx_codec") / get_dboard_fe_from_chan(chan_idx, uhd::TX_DIRECTION);
+ RFNOC_LOG_TRACE("Adding non-RFNoC block properties for channel "
+ << chan_idx << " to prop tree paths " << rx_codec_path << " and "
+ << tx_codec_path);
+
+ // ADC calibration state attributes
+ subtree->create<bool>(rx_codec_path / "calibration_frozen")
+ .add_coerced_subscriber([this, chan_idx](bool state) {
+ _rpcc->set_cal_frozen(state, get_block_id().get_block_count(), chan_idx);
+ })
+ .set_publisher([this, chan_idx]() {
+ const auto freeze_states =
+ _rpcc->get_cal_frozen(get_block_id().get_block_count(), chan_idx);
+ return freeze_states.at(0) == 1;
+ });
+
+ // RFDC NCO
+ // RX
+ subtree->create<double>(rx_codec_path / "rfdc" / "freq/value")
+ .add_desired_subscriber([this, chan_idx](double freq) {
+ _rpcc->rfdc_set_nco_freq(_get_trx_string(RX_DIRECTION),
+ get_block_id().get_block_count(),
+ chan_idx,
+ freq);
+ })
+ .set_publisher([this, chan_idx]() {
+ const auto nco_freq =
+ _rpcc->rfdc_get_nco_freq(_get_trx_string(RX_DIRECTION),
+ get_block_id().get_block_count(),
+ chan_idx);
+ return nco_freq;
+ });
+
+ // TX
+ subtree->create<double>(tx_codec_path / "rfdc" / "freq/value")
+ .add_desired_subscriber([this, chan_idx](double freq) {
+ _rpcc->rfdc_set_nco_freq(_get_trx_string(TX_DIRECTION),
+ get_block_id().get_block_count(),
+ chan_idx,
+ freq);
+ })
+ .set_publisher([this, chan_idx]() {
+ const auto nco_freq =
+ _rpcc->rfdc_get_nco_freq(_get_trx_string(TX_DIRECTION),
+ get_block_id().get_block_count(),
+ chan_idx);
+ return nco_freq;
+ });
+ }
+}
+
+void x400_radio_control_impl::_validate_master_clock_rate_args()
+{
+ auto block_args = get_block_args();
+
+ // 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.
+ const double master_clock_rate = _rpcc->get_master_clock_rate();
+ if (!uhd::math::frequencies_are_equal(get_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) % (get_rate() / 1e6)));
+ }
+ RFNOC_LOG_DEBUG("Master Clock Rate is: " << (master_clock_rate / 1e6) << " MHz.");
+}
+
+void x400_radio_control_impl::_init_mpm()
+{
+ // Init sensors
+ for (const auto& dir : std::vector<direction_t>{RX_DIRECTION, TX_DIRECTION}) {
+ // TODO: We should pull the number of channels from _daughterboard
+ for (size_t chan_idx = 0; chan_idx < uhd::usrp::zbx::ZBX_NUM_CHANS; chan_idx++) {
+ _init_mpm_sensors(dir, chan_idx);
+ }
+ }
+}
+
+// @TODO: This should be a method on direction_t
+// (or otherwise not duplicated from the implementation in zbx)
+std::string x400_radio_control_impl::_get_trx_string(const direction_t dir) const
+{
+ if (dir == RX_DIRECTION) {
+ return "rx";
+ } else if (dir == TX_DIRECTION) {
+ return "tx";
+ } else {
+ UHD_THROW_INVALID_CODE_PATH();
+ }
+}
+
+
+void x400_radio_control_impl::_init_mpm_sensors(
+ const direction_t dir, const size_t chan_idx)
+{
+ // TODO: We should pull the number of channels from _daughterboard
+ UHD_ASSERT_THROW(chan_idx < uhd::usrp::zbx::ZBX_NUM_CHANS);
+ const std::string trx = _get_trx_string(dir);
+ const fs_path fe_path = fs_path("dboard")
+ / (dir == RX_DIRECTION ? "rx_frontends" : "tx_frontends")
+ / chan_idx;
+ auto sensor_list = _db_rpcc->get_sensors(trx);
+ RFNOC_LOG_TRACE("Chan " << chan_idx << ": Found " << sensor_list.size() << " " << trx
+ << " sensors.");
+ for (const auto& sensor_name : sensor_list) {
+ RFNOC_LOG_TRACE("Adding " << trx << " sensor " << sensor_name);
+ get_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->_db_rpcc->get_sensor(trx, sensor_name, chan_idx));
+ });
+ }
+}
+
+fs_path x400_radio_control_impl::_get_db_fe_path(
+ const size_t chan, const direction_t dir) const
+{
+ const std::string trx = _get_trx_string(dir);
+ return DB_PATH / (trx + "_frontends") / get_dboard_fe_from_chan(chan, dir);
+}
+
+
+double x400_radio_control_impl::set_rate(const double rate)
+{
+ // X400 does not support runtime rate changes
+ if (!uhd::math::frequencies_are_equal(rate, get_rate())) {
+ RFNOC_LOG_WARNING("Requesting invalid sampling rate from device: "
+ << (rate / 1e6) << " MHz. Actual rate is: "
+ << (get_rate() / 1e6) << " MHz.");
+ }
+ return get_rate();
+}
+
+eeprom_map_t x400_radio_control_impl::get_db_eeprom()
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->get_db_eeprom();
+}
+
+std::vector<uhd::usrp::pwr_cal_mgr::sptr> x400_radio_control_impl::get_pwr_mgr(
+ const uhd::direction_t trx)
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->get_pwr_mgr(trx);
+}
+
+std::string x400_radio_control_impl::get_tx_antenna(const size_t chan) const
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->get_tx_antenna(chan);
+}
+
+std::vector<std::string> x400_radio_control_impl::get_tx_antennas(const size_t chan) const
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->get_tx_antennas(chan);
+}
+
+void x400_radio_control_impl::set_tx_antenna(const std::string& ant, const size_t chan)
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ _daughterboard->set_tx_antenna(ant, chan);
+}
+
+std::string x400_radio_control_impl::get_rx_antenna(const size_t chan) const
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->get_rx_antenna(chan);
+}
+
+std::vector<std::string> x400_radio_control_impl::get_rx_antennas(const size_t chan) const
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->get_rx_antennas(chan);
+}
+
+void x400_radio_control_impl::set_rx_antenna(const std::string& ant, const size_t chan)
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ _daughterboard->set_rx_antenna(ant, chan);
+}
+
+double x400_radio_control_impl::get_tx_frequency(const size_t chan)
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->get_tx_frequency(chan);
+}
+
+double x400_radio_control_impl::set_tx_frequency(const double freq, size_t chan)
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->set_tx_frequency(freq, chan);
+}
+
+void x400_radio_control_impl::set_tx_tune_args(
+ const uhd::device_addr_t& args, const size_t chan)
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ _daughterboard->set_tx_tune_args(args, chan);
+}
+
+uhd::freq_range_t x400_radio_control_impl::get_tx_frequency_range(const size_t chan) const
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->get_tx_frequency_range(chan);
+}
+
+double x400_radio_control_impl::get_rx_frequency(const size_t chan)
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->get_rx_frequency(chan);
+}
+
+double x400_radio_control_impl::set_rx_frequency(const double freq, const size_t chan)
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->set_rx_frequency(freq, chan);
+}
+
+void x400_radio_control_impl::set_rx_tune_args(
+ const uhd::device_addr_t& args, const size_t chan)
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ _daughterboard->set_rx_tune_args(args, chan);
+}
+
+uhd::freq_range_t x400_radio_control_impl::get_rx_frequency_range(const size_t chan) const
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->get_rx_frequency_range(chan);
+}
+
+std::vector<std::string> x400_radio_control_impl::get_tx_gain_names(
+ const size_t chan) const
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->get_tx_gain_names(chan);
+}
+
+uhd::gain_range_t x400_radio_control_impl::get_tx_gain_range(const size_t chan) const
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->get_tx_gain_range(chan);
+}
+
+uhd::gain_range_t x400_radio_control_impl::get_tx_gain_range(
+ const std::string& name, const size_t chan) const
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->get_tx_gain_range(name, chan);
+}
+
+double x400_radio_control_impl::get_tx_gain(const size_t chan)
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->get_tx_gain(chan);
+}
+
+double x400_radio_control_impl::get_tx_gain(const std::string& name, const size_t chan)
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->get_tx_gain(name, chan);
+}
+
+double x400_radio_control_impl::set_tx_gain(const double gain, const size_t chan)
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->set_tx_gain(gain, chan);
+}
+
+double x400_radio_control_impl::set_tx_gain(
+ const double gain, const std::string& name, const size_t chan)
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->set_tx_gain(gain, name, chan);
+}
+
+std::vector<std::string> x400_radio_control_impl::get_rx_gain_names(
+ const size_t chan) const
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->get_rx_gain_names(chan);
+}
+
+uhd::gain_range_t x400_radio_control_impl::get_rx_gain_range(const size_t chan) const
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->get_rx_gain_range(chan);
+}
+
+uhd::gain_range_t x400_radio_control_impl::get_rx_gain_range(
+ const std::string& name, const size_t chan) const
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->get_rx_gain_range(name, chan);
+}
+
+double x400_radio_control_impl::get_rx_gain(const size_t chan)
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->get_rx_gain(chan);
+}
+
+double x400_radio_control_impl::get_rx_gain(const std::string& name, const size_t chan)
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->get_rx_gain(name, chan);
+}
+
+double x400_radio_control_impl::set_rx_gain(const double gain, const size_t chan)
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->set_rx_gain(gain, chan);
+}
+
+double x400_radio_control_impl::set_rx_gain(
+ const double gain, const std::string& name, const size_t chan)
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->set_rx_gain(gain, name, chan);
+}
+
+void x400_radio_control_impl::set_rx_agc(const bool enable, const size_t chan)
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ _daughterboard->set_rx_agc(enable, chan);
+}
+
+meta_range_t x400_radio_control_impl::get_tx_bandwidth_range(size_t chan) const
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->get_tx_bandwidth_range(chan);
+}
+
+double x400_radio_control_impl::get_tx_bandwidth(const size_t chan)
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->get_tx_bandwidth(chan);
+}
+
+double x400_radio_control_impl::set_tx_bandwidth(
+ const double bandwidth, const size_t chan)
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->set_tx_bandwidth(bandwidth, chan);
+}
+
+meta_range_t x400_radio_control_impl::get_rx_bandwidth_range(size_t chan) const
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->get_rx_bandwidth_range(chan);
+}
+
+double x400_radio_control_impl::get_rx_bandwidth(const size_t chan)
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->get_rx_bandwidth(chan);
+}
+
+double x400_radio_control_impl::set_rx_bandwidth(
+ const double bandwidth, const size_t chan)
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->set_rx_bandwidth(bandwidth, chan);
+}
+
+std::vector<std::string> x400_radio_control_impl::get_rx_lo_names(const size_t chan) const
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->get_rx_lo_names(chan);
+}
+
+std::vector<std::string> x400_radio_control_impl::get_rx_lo_sources(
+ const std::string& name, const size_t chan) const
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->get_rx_lo_sources(name, chan);
+}
+
+freq_range_t x400_radio_control_impl::get_rx_lo_freq_range(
+ const std::string& name, const size_t chan) const
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->get_rx_lo_freq_range(name, chan);
+}
+
+void x400_radio_control_impl::set_rx_lo_source(
+ const std::string& src, const std::string& name, const size_t chan)
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ _daughterboard->set_rx_lo_source(src, name, chan);
+}
+
+const std::string x400_radio_control_impl::get_rx_lo_source(
+ const std::string& name, const size_t chan)
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->get_rx_lo_source(name, chan);
+}
+
+void x400_radio_control_impl::set_rx_lo_export_enabled(
+ bool enabled, const std::string& name, const size_t chan)
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ _daughterboard->set_rx_lo_export_enabled(enabled, name, chan);
+}
+
+bool x400_radio_control_impl::get_rx_lo_export_enabled(
+ const std::string& name, const size_t chan)
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->get_rx_lo_export_enabled(name, chan);
+}
+
+double x400_radio_control_impl::set_rx_lo_freq(
+ double freq, const std::string& name, const size_t chan)
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->set_rx_lo_freq(freq, name, chan);
+}
+
+double x400_radio_control_impl::get_rx_lo_freq(const std::string& name, const size_t chan)
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->get_rx_lo_freq(name, chan);
+}
+
+std::vector<std::string> x400_radio_control_impl::get_tx_lo_names(const size_t chan) const
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->get_tx_lo_names(chan);
+}
+
+std::vector<std::string> x400_radio_control_impl::get_tx_lo_sources(
+ const std::string& name, const size_t chan) const
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->get_tx_lo_sources(name, chan);
+}
+
+freq_range_t x400_radio_control_impl::get_tx_lo_freq_range(
+ const std::string& name, const size_t chan)
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->get_tx_lo_freq_range(name, chan);
+}
+
+void x400_radio_control_impl::set_tx_lo_source(
+ const std::string& src, const std::string& name, const size_t chan)
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ _daughterboard->set_tx_lo_source(src, name, chan);
+}
+
+const std::string x400_radio_control_impl::get_tx_lo_source(
+ const std::string& name, const size_t chan)
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->get_tx_lo_source(name, chan);
+}
+
+void x400_radio_control_impl::set_tx_lo_export_enabled(
+ const bool enabled, const std::string& name, const size_t chan)
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ _daughterboard->set_tx_lo_export_enabled(enabled, name, chan);
+}
+
+bool x400_radio_control_impl::get_tx_lo_export_enabled(
+ const std::string& name, const size_t chan)
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->get_tx_lo_export_enabled(name, chan);
+}
+
+double x400_radio_control_impl::set_tx_lo_freq(
+ const double freq, const std::string& name, const size_t chan)
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->set_tx_lo_freq(freq, name, chan);
+}
+
+double x400_radio_control_impl::get_tx_lo_freq(const std::string& name, const size_t chan)
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->get_tx_lo_freq(name, chan);
+}
+
+void x400_radio_control_impl::set_command_time(uhd::time_spec_t time, const size_t chan)
+{
+ node_t::set_command_time(time, chan);
+ _daughterboard->set_command_time(time, chan);
+}
+
+/**************************************************************************
+ * Sensor API
+ *************************************************************************/
+std::vector<std::string> x400_radio_control_impl::get_rx_sensor_names(
+ const size_t chan) const
+{
+ const fs_path sensor_path = _get_db_fe_path(chan, RX_DIRECTION) / "sensors";
+ if (get_tree()->exists(sensor_path)) {
+ return get_tree()->list(sensor_path);
+ }
+ return {};
+}
+
+uhd::sensor_value_t x400_radio_control_impl::get_rx_sensor(
+ const std::string& name, const size_t chan)
+{
+ return get_tree()
+ ->access<uhd::sensor_value_t>(
+ _get_db_fe_path(chan, RX_DIRECTION) / "sensors" / name)
+ .get();
+}
+
+std::vector<std::string> x400_radio_control_impl::get_tx_sensor_names(
+ const size_t chan) const
+{
+ const fs_path sensor_path = _get_db_fe_path(chan, TX_DIRECTION) / "sensors";
+ if (get_tree()->exists(sensor_path)) {
+ return get_tree()->list(sensor_path);
+ }
+ return {};
+}
+
+uhd::sensor_value_t x400_radio_control_impl::get_tx_sensor(
+ const std::string& name, const size_t chan)
+{
+ return get_tree()
+ ->access<uhd::sensor_value_t>(
+ _get_db_fe_path(chan, TX_DIRECTION) / "sensors" / name)
+ .get();
+}
+
+/**************************************************************************
+ * Radio Identification API Calls
+ *************************************************************************/
+size_t x400_radio_control_impl::get_chan_from_dboard_fe(
+ const std::string& fe, const uhd::direction_t direction) const
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->get_chan_from_dboard_fe(fe, direction);
+}
+
+std::string x400_radio_control_impl::get_dboard_fe_from_chan(
+ const size_t chan, const uhd::direction_t direction) const
+{
+ std::lock_guard<std::recursive_mutex> l(_lock);
+ return _daughterboard->get_dboard_fe_from_chan(chan, direction);
+}
+
+UHD_RFNOC_BLOCK_REGISTER_FOR_DEVICE_DIRECT(
+ x400_radio_control, RADIO_BLOCK, X400, "Radio", true, "radio_clk", "ctrl_clk")
+
+}} // namespace uhd::rfnoc
diff --git a/host/lib/usrp/x400/x400_radio_control.hpp b/host/lib/usrp/x400/x400_radio_control.hpp
new file mode 100644
index 000000000..65b37cc2b
--- /dev/null
+++ b/host/lib/usrp/x400/x400_radio_control.hpp
@@ -0,0 +1,198 @@
+//
+// Copyright 2020 Ettus Research, a National Instruments Brand
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+#pragma once
+
+#include "adc_self_calibration.hpp"
+#include <uhd/rfnoc/noc_block_base.hpp>
+#include <uhd/types/device_addr.hpp>
+#include <uhd/types/direction.hpp>
+#include <uhd/types/ranges.hpp>
+#include <uhdlib/features/fpga_load_notification_iface.hpp>
+#include <uhdlib/rfnoc/radio_control_impl.hpp>
+#include <uhdlib/rfnoc/rf_control/dboard_iface.hpp>
+#include <uhdlib/usrp/common/mpmd_mb_controller.hpp>
+#include <uhdlib/usrp/common/rpc.hpp>
+#include <uhdlib/usrp/common/x400_rfdc_control.hpp>
+#include <uhdlib/utils/rpc.hpp>
+#include <stddef.h>
+#include <memory>
+#include <mutex>
+#include <string>
+#include <vector>
+
+namespace uhd { namespace rfnoc {
+
+namespace x400_regs {
+
+//! Base address for the rf_timing_control module. This controls the NCOs and
+// other things in the RFDC.
+constexpr uint32_t RFDC_CTRL_BASE = radio_control_impl::regmap::PERIPH_BASE + 0x8000;
+
+} // namespace x400_regs
+
+class x400_radio_control_impl : public radio_control_impl
+{
+public:
+ using sptr = std::shared_ptr<x400_radio_control_impl>;
+
+ /************************************************************************
+ * Structors
+ ***********************************************************************/
+ x400_radio_control_impl(make_args_ptr make_args);
+ virtual ~x400_radio_control_impl() = default;
+
+ std::string get_slot_name() const override
+ {
+ return _radio_slot;
+ }
+
+ size_t get_chan_from_dboard_fe(const std::string&, uhd::direction_t) const override;
+ std::string get_dboard_fe_from_chan(size_t chan, uhd::direction_t) const override;
+
+ uhd::eeprom_map_t get_db_eeprom() override;
+
+ // Shim calls for every method in rf_control_core
+ double set_rate(const double rate) override;
+ std::string get_tx_antenna(const size_t chan) const override;
+ std::vector<std::string> get_tx_antennas(const size_t chan) const override;
+ void set_tx_antenna(const std::string& ant, const size_t chan) override;
+ std::string get_rx_antenna(const size_t chan) const override;
+ std::vector<std::string> get_rx_antennas(const size_t chan) const override;
+ void set_rx_antenna(const std::string& ant, const size_t chan) override;
+ double get_tx_frequency(const size_t chan) override;
+ double set_tx_frequency(const double freq, size_t chan) override;
+ void set_tx_tune_args(const uhd::device_addr_t& args, const size_t chan) override;
+ uhd::freq_range_t get_tx_frequency_range(const size_t chan) const override;
+ double get_rx_frequency(const size_t chan) override;
+ double set_rx_frequency(const double freq, const size_t chan) override;
+ void set_rx_tune_args(const uhd::device_addr_t& args, const size_t chan) override;
+ uhd::freq_range_t get_rx_frequency_range(const size_t chan) const override;
+ std::vector<std::string> get_tx_gain_names(const size_t chan) const override;
+ uhd::gain_range_t get_tx_gain_range(const size_t chan) const override;
+ uhd::gain_range_t get_tx_gain_range(
+ const std::string& name, const size_t chan) const override;
+ double get_tx_gain(const size_t chan) override;
+ double get_tx_gain(const std::string& name, const size_t chan) override;
+ double set_tx_gain(const double gain, const size_t chan) override;
+ double set_tx_gain(
+ const double gain, const std::string& name, const size_t chan) override;
+ std::vector<std::string> get_rx_gain_names(const size_t chan) const override;
+ uhd::gain_range_t get_rx_gain_range(const size_t chan) const override;
+ uhd::gain_range_t get_rx_gain_range(
+ const std::string& name, const size_t chan) const override;
+ double get_rx_gain(const size_t chan) override;
+ double get_rx_gain(const std::string& name, const size_t chan) override;
+ double set_rx_gain(const double gain, const size_t chan) override;
+ double set_rx_gain(
+ const double gain, const std::string& name, const size_t chan) override;
+ void set_rx_agc(const bool enable, const size_t chan) override;
+ meta_range_t get_tx_bandwidth_range(size_t chan) const override;
+ double get_tx_bandwidth(const size_t chan) override;
+ double set_tx_bandwidth(const double bandwidth, const size_t chan) override;
+ meta_range_t get_rx_bandwidth_range(size_t chan) const override;
+ double get_rx_bandwidth(const size_t chan) override;
+ double set_rx_bandwidth(const double bandwidth, const size_t chan) override;
+
+ std::vector<std::string> get_rx_lo_names(const size_t chan) const override;
+ std::vector<std::string> get_rx_lo_sources(
+ const std::string& name, const size_t chan) const override;
+ freq_range_t get_rx_lo_freq_range(
+ const std::string& name, const size_t chan) const override;
+ void set_rx_lo_source(
+ const std::string& src, const std::string& name, const size_t chan) override;
+ const std::string get_rx_lo_source(
+ const std::string& name, const size_t chan) override;
+ void set_rx_lo_export_enabled(
+ bool enabled, const std::string& name, const size_t chan) override;
+ bool get_rx_lo_export_enabled(
+ const std::string& name, const size_t chan) override;
+ double set_rx_lo_freq(
+ double freq, const std::string& name, const size_t chan) override;
+ double get_rx_lo_freq(const std::string& name, const size_t chan) override;
+ std::vector<std::string> get_tx_lo_names(const size_t chan) const override;
+ std::vector<std::string> get_tx_lo_sources(
+ const std::string& name, const size_t chan) const override;
+ freq_range_t get_tx_lo_freq_range(
+ const std::string& name, const size_t chan) override;
+ void set_tx_lo_source(
+ const std::string& src, const std::string& name, const size_t chan) override;
+ const std::string get_tx_lo_source(
+ const std::string& name, const size_t chan) override;
+ void set_tx_lo_export_enabled(
+ const bool enabled, const std::string& name, const size_t chan) override;
+ bool get_tx_lo_export_enabled(const std::string& name, const size_t chan) override;
+ double set_tx_lo_freq(
+ const double freq, const std::string& name, const size_t chan) override;
+ double get_tx_lo_freq(const std::string& name, const size_t chan) override;
+ std::vector<std::string> get_rx_sensor_names(size_t chan) const override;
+ uhd::sensor_value_t get_rx_sensor(const std::string& name, size_t chan) override;
+ std::vector<std::string> get_tx_sensor_names(size_t chan) const override;
+ uhd::sensor_value_t get_tx_sensor(const std::string& name, size_t chan) override;
+
+ void set_command_time(uhd::time_spec_t time, const size_t chan) override;
+
+ // Non-API methods
+ // This is used for x4xx radio block unit tests
+ std::vector<uhd::usrp::pwr_cal_mgr::sptr> get_pwr_mgr(const uhd::direction_t trx);
+
+private:
+ //! Locks access to the API
+ mutable std::recursive_mutex _lock;
+
+ std::string _get_trx_string(const direction_t dir) const;
+
+ void _validate_master_clock_rate_args();
+ void _init_mpm();
+ void _init_mpm_sensors(const direction_t dir, const size_t chan_idx);
+ void _init_prop_tree();
+ fs_path _get_db_fe_path(const size_t chan, const uhd::direction_t dir) const;
+
+ //! Reference to the MB controller
+ uhd::rfnoc::mpmd_mb_controller::sptr _mb_control;
+
+ //! Reference to the RPC client
+ uhd::usrp::x400_rpc_iface::sptr _rpcc;
+ uhd::usrp::dboard_base_rpc_iface::sptr _db_rpcc;
+
+ //! Reference to the MB timekeeper
+ uhd::rfnoc::mpmd_mb_controller::mpmd_timekeeper::sptr _x4xx_timekeeper;
+
+ std::string _radio_slot;
+ std::string _rpc_prefix;
+
+ //! Reference to this radio block's RFDC control
+ x400::rfdc_control::sptr _rfdcc;
+
+ uhd::usrp::x400::x400_dboard_iface::sptr _daughterboard;
+
+ uhd::features::adc_self_calibration_iface::sptr _adc_self_calibration;
+
+ class fpga_onload : public uhd::features::fpga_load_notification_iface
+ {
+ public:
+ using sptr = std::shared_ptr<fpga_onload>;
+
+ fpga_onload(size_t num_channels,
+ uhd::features::adc_self_calibration_iface::sptr adc_self_cal,
+ std::string unique_id);
+
+ void onload() override;
+
+ private:
+ const size_t _num_channels;
+ uhd::features::adc_self_calibration_iface::sptr _adc_self_cal;
+ const std::string _unique_id;
+ std::string get_unique_id() const
+ {
+ return _unique_id;
+ }
+ };
+
+ fpga_onload::sptr _fpga_onload;
+};
+
+}} // namespace uhd::rfnoc
diff --git a/host/lib/usrp/x400/x400_rfdc_control.cpp b/host/lib/usrp/x400/x400_rfdc_control.cpp
new file mode 100644
index 000000000..b963c0a46
--- /dev/null
+++ b/host/lib/usrp/x400/x400_rfdc_control.cpp
@@ -0,0 +1,73 @@
+//
+// Copyright 2020 Ettus Research, a National Instruments Brand
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+#include <uhd/utils/log.hpp>
+#include <uhdlib/usrp/common/x400_rfdc_control.hpp>
+#include <unordered_map>
+
+using namespace uhd::rfnoc::x400;
+
+rfdc_control::rfdc_control(uhd::memmap32_iface_timed&& iface, const std::string& log_id)
+ : _iface(std::move(iface)), _log_id(log_id)
+{
+ // nop
+}
+
+void rfdc_control::reset_ncos(
+ const std::vector<rfdc_type>& ncos, const uhd::time_spec_t& time)
+{
+ if (ncos.empty()) {
+ UHD_LOG_WARNING(_log_id,
+ "reset_ncos() called with empty NCO list! "
+ "Not resetting NCOs.");
+ return;
+ }
+ UHD_LOG_TRACE(_log_id, "Resetting " << ncos.size() << " NCOs...");
+
+ const uint32_t reset_word = 1;
+ // TODO: When the FPGA supports it, map the list of ncos into bits onto
+ // reset_word to set the bit for the specific NCOs that will be reset.
+
+ _iface.poke32(regmap::NCO_RESET, reset_word, time);
+}
+
+void rfdc_control::reset_gearboxes(
+ const std::vector<rfdc_type>& gearboxes, const uhd::time_spec_t& time)
+{
+ if (gearboxes.empty()) {
+ UHD_LOG_WARNING(_log_id,
+ "reset_gearboxes() called with empty gearbox list! "
+ "Not resetting gearboxes.");
+ return;
+ }
+ // This is intentionally at INFO: It should typically happen once per session.
+ UHD_LOG_INFO(_log_id, "Resetting " << gearboxes.size() << " gearbox(es)...");
+ // TODO: Either the FPGA supports resetting gearboxes individually, or we
+ // remove this TODO
+ static const std::unordered_map<rfdc_type, const uint32_t> gb_map{
+ {rfdc_type::TX0, regmap::DAC_RESET_MSB},
+ {rfdc_type::TX1, regmap::DAC_RESET_MSB},
+ {rfdc_type::RX0, regmap::ADC_RESET_MSB},
+ {rfdc_type::RX1, regmap::ADC_RESET_MSB}};
+
+ uint32_t reset_word = 0;
+ for (const auto gb : gearboxes) {
+ reset_word = reset_word | (1 << gb_map.at(gb));
+ }
+
+ _iface.poke32(regmap::GEARBOX_RESET, reset_word, time);
+}
+
+bool rfdc_control::get_nco_reset_done()
+{
+ return bool(_iface.peek32(regmap::NCO_RESET) & (1 << regmap::NCO_RESET_DONE_MSB));
+}
+
+double rfdc_control::set_nco_freq(const rfdc_type, const double freq)
+{
+ UHD_LOG_WARNING(_log_id, "set_nco_freq() called but not yet implemented!");
+ return freq;
+}