aboutsummaryrefslogtreecommitdiffstats
path: root/host/lib
diff options
context:
space:
mode:
authorMartin Braun <martin.braun@ettus.com>2020-04-15 15:07:53 -0700
committerAaron Rossetto <aaron.rossetto@ni.com>2020-05-19 13:24:59 -0500
commit2d7676e8cf2cc4fb59e59185864c8804ca77e15b (patch)
tree765965e19b86f37077b19b4ea4eff46c8b68c77c /host/lib
parent676c3a37867f0e19476e9dc8495127bb3208a1a0 (diff)
downloaduhd-2d7676e8cf2cc4fb59e59185864c8804ca77e15b.tar.gz
uhd-2d7676e8cf2cc4fb59e59185864c8804ca77e15b.tar.bz2
uhd-2d7676e8cf2cc4fb59e59185864c8804ca77e15b.zip
lib: Add power cal manager
This is a utility class that can be used by USRP or daughterboard drivers to tie power calibration into their respective drivers.
Diffstat (limited to 'host/lib')
-rw-r--r--host/lib/include/uhdlib/usrp/common/pwr_cal_mgr.hpp151
-rw-r--r--host/lib/usrp/common/CMakeLists.txt1
-rw-r--r--host/lib/usrp/common/pwr_cal_mgr.cpp274
3 files changed, 426 insertions, 0 deletions
diff --git a/host/lib/include/uhdlib/usrp/common/pwr_cal_mgr.hpp b/host/lib/include/uhdlib/usrp/common/pwr_cal_mgr.hpp
new file mode 100644
index 000000000..4d063825b
--- /dev/null
+++ b/host/lib/include/uhdlib/usrp/common/pwr_cal_mgr.hpp
@@ -0,0 +1,151 @@
+//
+// Copyright 2020 Ettus Research, a National Instruments Brand
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+#pragma once
+
+#include <uhd/property_tree.hpp>
+#include <uhd/types/ranges.hpp>
+#include <uhd/utils/gain_group.hpp>
+#include <functional>
+#include <memory>
+#include <string>
+
+namespace uhd { namespace usrp {
+
+/*! Power Calibration Manager: Helper class to set track power vs. gain settings
+ *
+ * This class can be plugged into USRP device drivers (or daughterboard drivers).
+ * It will manage loading the calibration data, and translating power settings
+ * into gain settings, and vice versa (i.e. read the power from a gain setting).
+ *
+ * This class mostly communicates with the device driver via callbacks. This
+ * allows separating the power control from the device driver without having to
+ * implement any device-specific code in this manager.
+ *
+ * The actual control of gains is also done via callbacks, but they are
+ * enapsulated into a gain group. The gain group must separate hardware gains
+ * from other gains that can be used for more fine-grained correction; the
+ * existing gain groups in device drivers may or may not be adequate.
+ *
+ * For example, the X3x0 has two gain stages on the RX side, there's an ADC gain
+ * as well as the analog gain of the daughterboards. The calibration is performed
+ * over the entire gain range, including the ADC gain, which means that the
+ * hardware gains *include* the ADC gain and must therefore not be a separate gain
+ * stage for this class.
+ */
+class pwr_cal_mgr
+{
+public:
+ using sptr = std::shared_ptr<pwr_cal_mgr>;
+ using get_double_type = std::function<double(void)>;
+ using get_str_type = std::function<std::string(void)>;
+ //! The current power/gain tracking mode. When the device does something
+ // that will cause the output power to change, we have an option of keeping
+ // the gain constant, or keeping the power constant.
+ enum class tracking_mode {
+ TRACK_GAIN, //!< In this mode, we keep the gain after retuning.
+ TRACK_POWER //!< In this mode, we keep the power after retuning
+ };
+
+ //! Helper: Sanitize antenna names (e.g. TX/RX -> tx_rx)
+ //
+ // Note: Argument is not const std::string&; we make use of C++'s short
+ // string optimization for an easier implementation
+ static std::string sanitize_antenna_name(std::string antenna_name);
+
+ //! Helper: Check if an antenna name is valid for calibration (e.g., CAL and
+ // LOCAL are not)
+ static bool is_valid_antenna(const std::string& antenna);
+
+ /*! Factory
+ *
+ * \param serial The serial string used for the cal database lookup
+ * \param log_id A string used for logging
+ * \param get_freq A function object to read back the current frequency from the
+ * underlying device
+ * \param get_key A function object to read back the current calibraton key
+ * from the underlying device
+ * \param gain_group A gain group. The first gain stage (with the
+ * higher priority) must be the same gain as used in the
+ * calibration data; usually, it's the overall hardware
+ * gain. The second is any ancillary gain that can be used
+ * for correction. This could be a baseband gain.
+ */
+ static sptr make(const std::string& serial,
+ const std::string& log_id,
+ get_double_type&& get_freq,
+ get_str_type&& get_key,
+ uhd::gain_group::sptr gain_group);
+
+ //! Update the gain group (see make());
+ //
+ // Not thread-safe: Don't call at the same time as set_power()
+ virtual void set_gain_group(uhd::gain_group::sptr gain_group) = 0;
+
+ //! Return true if there is power cal data for the currently selected port
+ virtual bool has_power_data() = 0;
+
+ //! Add a property tree node (ref_power/range and ref_power/value)
+ //
+ // For non-RFNoC devices (e.g. B200), this must be called in order to
+ // expose the power APIs to multi_usrp.
+ virtual void populate_subtree(uhd::property_tree::sptr subtree) = 0;
+
+ /*! Set the power to \p power_dbm
+ *
+ * This function will coerce both the valid power, and the resulting gain
+ * value, and will return the actual, current power.
+ *
+ * Note: Calling this will automatically set the tracking mode to TRACK_POWER.
+ */
+ virtual double set_power(const double power_dbm) = 0;
+
+ //! Return the current power
+ virtual double get_power() = 0;
+
+ /*! Reapply the power setting from the last set_power() call
+ *
+ * This is useful for reapplying settings, e.g., after tuning. It can be
+ * set as a callback or subscriber to events that require a power reset.
+ * This will do nothing if the current tracking mode is set to
+ * tracking_mode::KEEP_GAIN
+ */
+ virtual void update_power() = 0;
+
+ /*! Return the valid range of powers
+ *
+ * This will query the device state and return currently valid settings.
+ */
+ virtual uhd::meta_range_t get_power_range() = 0;
+
+ //! Update the temperature in Celsius
+ //
+ // Because reading the temperature can be an invasive operation, we leave it
+ // up to the device implementation to update the temperature at sensible
+ // intervals instead of using a callback.
+ virtual void set_temperature(const int temp_C) = 0;
+
+ /*! Set the current power tracking mode
+ */
+ virtual void set_tracking_mode(const tracking_mode) = 0;
+
+ //! Return the currently active calibration data key
+ virtual std::string get_key() = 0;
+
+ //! Return the calibration serial
+ virtual std::string get_serial() const = 0;
+
+ //! Update serial
+ //
+ // This may be called for example when the hardware is hot-pluggable, or
+ // if the calibration key changes at runtime.
+ // Calling it will not only set the serial number, but will also force a
+ // reload of the calibration data. The existing calibration data is
+ // discarded.
+ virtual void set_serial(const std::string& serial) = 0;
+};
+
+}} // namespace uhd::usrp
diff --git a/host/lib/usrp/common/CMakeLists.txt b/host/lib/usrp/common/CMakeLists.txt
index 7a6a4fe27..a61738743 100644
--- a/host/lib/usrp/common/CMakeLists.txt
+++ b/host/lib/usrp/common/CMakeLists.txt
@@ -35,6 +35,7 @@ LIBUHD_APPEND_SOURCES(
${CMAKE_CURRENT_SOURCE_DIR}/recv_packet_demuxer.cpp
${CMAKE_CURRENT_SOURCE_DIR}/io_service_mgr.cpp
${CMAKE_CURRENT_SOURCE_DIR}/io_service_args.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/pwr_cal_mgr.cpp
)
if(ENABLE_DPDK)
diff --git a/host/lib/usrp/common/pwr_cal_mgr.cpp b/host/lib/usrp/common/pwr_cal_mgr.cpp
new file mode 100644
index 000000000..1fdd15645
--- /dev/null
+++ b/host/lib/usrp/common/pwr_cal_mgr.cpp
@@ -0,0 +1,274 @@
+//
+// Copyright 2020 Ettus Research, a National Instruments Brand
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+#include <uhd/cal/database.hpp>
+#include <uhd/cal/pwr_cal.hpp>
+#include <uhd/types/direction.hpp>
+#include <uhd/utils/log.hpp>
+#include <uhd/utils/math.hpp>
+#include <uhdlib/usrp/common/pwr_cal_mgr.hpp>
+#include <unordered_map>
+#include <boost/algorithm/string.hpp>
+#include <boost/range/adaptor/filtered.hpp>
+#include <algorithm>
+#include <set>
+
+using namespace uhd::usrp;
+
+namespace {
+
+// List of antenna names that are globally known to never have their own cal data
+const std::set<std::string> INVALID_ANTENNAS{"CAL", "LOCAL"};
+
+} // namespace
+
+std::string pwr_cal_mgr::sanitize_antenna_name(std::string antenna_name)
+{
+ std::replace(antenna_name.begin(), antenna_name.end(), '/', '+');
+ boost::to_lower(antenna_name);
+ return antenna_name;
+}
+
+// Shorthand for filtering against INVALID_ANTENNAS
+bool pwr_cal_mgr::is_valid_antenna(const std::string& antenna)
+{
+ return !INVALID_ANTENNAS.count(antenna);
+}
+
+class pwr_cal_mgr_impl : public pwr_cal_mgr
+{
+public:
+ pwr_cal_mgr_impl(const std::string& serial,
+ const std::string& log_id,
+ get_double_type&& get_freq,
+ get_str_type&& get_key,
+ uhd::gain_group::sptr gain_group)
+ : _log_id(log_id)
+ , _get_freq(std::move(get_freq))
+ , _get_key(std::move(get_key))
+ , _gain_group(gain_group)
+ , _hw_gain_name(gain_group->get_names().at(0))
+ {
+ set_serial(serial);
+ }
+
+ void set_gain_group(uhd::gain_group::sptr gain_group)
+ {
+ _gain_group = gain_group;
+ }
+
+ bool has_power_data()
+ {
+ const std::string key = _get_key();
+ _load_cal_data(key);
+ return _cal_data.count(key) && bool(_cal_data.at(key));
+ }
+
+ void populate_subtree(uhd::property_tree::sptr subtree)
+ {
+ subtree->create<std::string>(uhd::fs_path("ref_power/key"))
+ .set_coercer([](const std::string&) -> std::string {
+ throw uhd::runtime_error("Cannot overwrite power cal key!");
+ })
+ .set_publisher([this]() { return _get_key(); });
+ subtree->create<std::string>(uhd::fs_path("ref_power/serial"))
+ .set_coercer([](const std::string&) -> std::string {
+ throw uhd::runtime_error("Cannot overwrite cal serial!");
+ })
+ .set_publisher([this]() { return _serial; });
+ if (!has_power_data()) {
+ return;
+ }
+ subtree->create<double>(uhd::fs_path("ref_power/value"))
+ .set_coercer(
+ [this](const double power_dbm) { return this->set_power(power_dbm); })
+ .set_publisher([this]() { return this->get_power(); });
+ subtree->create<uhd::meta_range_t>(uhd::fs_path("ref_power/range"))
+ .set_coercer([](const uhd::meta_range_t&) -> uhd::meta_range_t {
+ throw uhd::runtime_error("Cannot overwrite power range!");
+ })
+ .set_publisher([this]() { return this->get_power_range(); });
+ }
+
+ double set_power(const double power_dbm)
+ {
+ const std::string key = _get_key();
+ _load_cal_data(key);
+ UHD_ASSERT_THROW(_cal_data.count(key));
+ _desired_power = power_dbm;
+ const uint64_t freq = static_cast<uint64_t>(_get_freq());
+ auto& cal_data = _cal_data.at(key);
+ if (!cal_data) {
+ const std::string err_msg = std::string("Attempting to set power for key ")
+ + key + ", but no cal data available!";
+ UHD_LOG_ERROR(_log_id, err_msg);
+ throw uhd::runtime_error(err_msg);
+ }
+
+ const double desired_hw_gain = cal_data->get_gain(power_dbm, freq);
+ // This sets all the gains
+ _gain_group->set_value(desired_hw_gain);
+ const double coerced_hw_gain = _gain_group->get_value(_hw_gain_name);
+ const double coerced_hw_power = cal_data->get_power(coerced_hw_gain, freq);
+ const double coerced_total_gain = _gain_group->get_value();
+ const double coerced_total_power =
+ coerced_hw_power + coerced_total_gain - coerced_hw_gain;
+ UHD_LOG_TRACE(_log_id,
+ "Desired power: " << power_dbm << " dBm -> desired gain: " << desired_hw_gain
+ << " dB; Actual HW power: " << coerced_hw_power
+ << " dBm -> actual HW gain: " << coerced_hw_gain
+ << " dB, Actual total power: " << coerced_total_power
+ << " dBm -> actual total gain: " << coerced_total_gain
+ << " dB");
+ // We directly scale the power with the residual gain
+ return coerced_total_power;
+ }
+
+ double get_power()
+ {
+ const std::string key = _get_key();
+ _load_cal_data(key);
+ UHD_ASSERT_THROW(_cal_data.count(key));
+ auto& cal_data = _cal_data.at(key);
+ if (!cal_data) {
+ const std::string err_msg = std::string("Attempting to get power for key ")
+ + key + ", but no cal data available!";
+ UHD_LOG_ERROR(_log_id, err_msg);
+ throw uhd::runtime_error(err_msg);
+ }
+
+ const uint64_t freq = static_cast<uint64_t>(_get_freq());
+ const double hw_gain = _gain_group->get_value(_hw_gain_name);
+ const double hw_power = cal_data->get_power(hw_gain, freq);
+ // We directly scale the power with the residual gain
+ return hw_power + (_gain_group->get_value() - hw_gain);
+ }
+
+ void update_power()
+ {
+ if (_mode == tracking_mode::TRACK_POWER) {
+ set_power(_desired_power);
+ }
+ }
+
+ uhd::meta_range_t get_power_range()
+ {
+ const std::string key = _get_key();
+ _load_cal_data(key);
+ UHD_ASSERT_THROW(_cal_data.count(key));
+ auto& cal_data = _cal_data.at(key);
+ if (!cal_data) {
+ const std::string err_msg = std::string("Attempting to get power range for key ")
+ + key + ", but no cal data available!";
+ UHD_LOG_ERROR(_log_id, err_msg);
+ throw uhd::runtime_error(err_msg);
+ }
+ const uint64_t freq = static_cast<uint64_t>(_get_freq());
+ return cal_data->get_power_limits(freq);
+ }
+
+ void set_temperature(const int temp_C)
+ {
+ for (auto& cal_data : _cal_data) {
+ if (cal_data.second) {
+ cal_data.second->set_temperature(temp_C);
+ }
+ }
+ }
+
+ void set_tracking_mode(const tracking_mode mode)
+ {
+ _mode = mode;
+ }
+
+ void set_serial(const std::string& serial)
+ {
+ if (serial == _serial || serial.empty()) {
+ return;
+ }
+ _serial = serial;
+ _cal_data.clear();
+ }
+
+ void _load_cal_data(const std::string& key)
+ {
+ if (_cal_data.count(key)) {
+ return;
+ }
+ cal::pwr_cal::sptr cal_data(nullptr);
+ UHD_LOG_TRACE(
+ _log_id, "Looking for power cal data for " << key << ", serial " << _serial);
+ bool cal_data_found = false;
+ if (cal::database::has_cal_data(key, _serial)) {
+ try {
+ cal_data = cal::container::make<cal::pwr_cal>(
+ cal::database::read_cal_data(key, _serial));
+ cal_data_found = true;
+ } catch (const uhd::exception& ex) {
+ UHD_LOG_WARNING(_log_id, "Error loading cal data: " << ex.what());
+ }
+ }
+ _cal_data.insert({key, cal_data});
+ UHD_LOG_TRACE(_log_id,
+ (bool(cal_data) ? "" : "No ") << "power cal data found for key " << key
+ << ", key " << key << ", serial " << _serial);
+ if (cal_data_found) {
+ // If we found cal data, check that all the other antennas/keys also
+ // have cal data
+ if (std::any_of(_cal_data.cbegin(),
+ _cal_data.cend(),
+ [](const cal_data_map_type::value_type& data) {
+ return bool(data.second);
+ })) {
+ UHD_LOG_WARNING(_log_id,
+ "Some ports for " << _serial
+ << " have power cal data, others do not. This "
+ "will cause inconsistent behaviour across "
+ "antenna ports when setting power levels.");
+ }
+ }
+ }
+
+ std::string get_serial() const
+ {
+ return _serial;
+ }
+
+ std::string get_key()
+ {
+ return _get_key();
+ }
+
+private:
+ const std::string _log_id;
+ std::string _serial;
+
+ get_double_type _get_freq;
+ get_str_type _get_key;
+ uhd::gain_group::sptr _gain_group;
+ const std::string _hw_gain_name;
+
+ //! Store the cal data for every cal key
+ using cal_data_map_type =
+ std::unordered_map<std::string /* key */, uhd::usrp::cal::pwr_cal::sptr>;
+ cal_data_map_type _cal_data;
+
+ double _desired_power = 0;
+ tracking_mode _mode = tracking_mode::TRACK_GAIN;
+};
+
+pwr_cal_mgr::sptr pwr_cal_mgr::make(const std::string& serial,
+ const std::string& log_id,
+ pwr_cal_mgr::get_double_type&& get_freq,
+ pwr_cal_mgr::get_str_type&& get_key,
+ uhd::gain_group::sptr gain_group)
+{
+ return std::make_shared<pwr_cal_mgr_impl>(serial,
+ log_id,
+ std::move(get_freq),
+ std::move(get_key),
+ gain_group);
+}