aboutsummaryrefslogtreecommitdiffstats
path: root/host/lib/usrp/common/lmx2572.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'host/lib/usrp/common/lmx2572.cpp')
-rw-r--r--host/lib/usrp/common/lmx2572.cpp1030
1 files changed, 1030 insertions, 0 deletions
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));
+}