diff options
author | Lars Amsel <lars.amsel@ni.com> | 2021-06-04 08:27:50 +0200 |
---|---|---|
committer | Aaron Rossetto <aaron.rossetto@ni.com> | 2021-06-10 12:01:53 -0500 |
commit | 2a575bf9b5a4942f60e979161764b9e942699e1e (patch) | |
tree | 2f0535625c30025559ebd7494a4b9e7122550a73 /host/lib/usrp/dboard/zbx | |
parent | e17916220cc955fa219ae37f607626ba88c4afe3 (diff) | |
download | uhd-2a575bf9b5a4942f60e979161764b9e942699e1e.tar.gz uhd-2a575bf9b5a4942f60e979161764b9e942699e1e.tar.bz2 uhd-2a575bf9b5a4942f60e979161764b9e942699e1e.zip |
uhd: Add support for the USRP X410
Co-authored-by: Lars Amsel <lars.amsel@ni.com>
Co-authored-by: Michael Auchter <michael.auchter@ni.com>
Co-authored-by: Martin Braun <martin.braun@ettus.com>
Co-authored-by: Paul Butler <paul.butler@ni.com>
Co-authored-by: Cristina Fuentes <cristina.fuentes-curiel@ni.com>
Co-authored-by: Humberto Jimenez <humberto.jimenez@ni.com>
Co-authored-by: Virendra Kakade <virendra.kakade@ni.com>
Co-authored-by: Lane Kolbly <lane.kolbly@ni.com>
Co-authored-by: Max Köhler <max.koehler@ni.com>
Co-authored-by: Andrew Lynch <andrew.lynch@ni.com>
Co-authored-by: Grant Meyerhoff <grant.meyerhoff@ni.com>
Co-authored-by: Ciro Nishiguchi <ciro.nishiguchi@ni.com>
Co-authored-by: Thomas Vogel <thomas.vogel@ni.com>
Diffstat (limited to 'host/lib/usrp/dboard/zbx')
-rw-r--r-- | host/lib/usrp/dboard/zbx/CMakeLists.txt | 17 | ||||
-rw-r--r-- | host/lib/usrp/dboard/zbx/zbx_cpld_ctrl.cpp | 931 | ||||
-rw-r--r-- | host/lib/usrp/dboard/zbx/zbx_dboard.cpp | 758 | ||||
-rw-r--r-- | host/lib/usrp/dboard/zbx/zbx_dboard_init.cpp | 685 | ||||
-rw-r--r-- | host/lib/usrp/dboard/zbx/zbx_expert.cpp | 672 | ||||
-rw-r--r-- | host/lib/usrp/dboard/zbx/zbx_lo_ctrl.cpp | 162 |
6 files changed, 3225 insertions, 0 deletions
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 |