// // Copyright 2020 Ettus Research, A National Instruments Brand // // SPDX-License-Identifier: GPL-3.0-or-later // #include "lmx2572_regs.hpp" #include #include #include #include #include #include #include #include #include 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 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(); 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(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(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(std::max(1.0, std::min(std::ceil(fPD * p / delta_fVCO), double(std::numeric_limits::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(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(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(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(); 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 > 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(mash_rst_count >> 16); _regs.mash_rst_count_lower = uhd::narrow_cast(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 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(mash_seed >> 16); _regs.mash_seed_lower = uhd::narrow_cast(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(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(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(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 > 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(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 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 > 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((n >> 16) & 0x7); _regs.pll_n_lower_16_bits = uhd::narrow_cast(n); } void _set_pll_num(const uint32_t num) { _regs.pll_num_upper = uhd::narrow_cast(num >> 16); _regs.pll_num_lower = uhd::narrow_cast(num); } void _set_pll_den(const uint32_t den) { _regs.pll_den_upper = uhd::narrow_cast(den >> 16); _regs.pll_den_lower = uhd::narrow_cast(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( std::move(poke_fn), std::move(peek_fn), std::move(sleep_fn)); }