diff options
| -rw-r--r-- | host/lib/include/uhdlib/usrp/common/pwr_cal_mgr.hpp | 151 | ||||
| -rw-r--r-- | host/lib/usrp/common/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | host/lib/usrp/common/pwr_cal_mgr.cpp | 274 | 
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); +} | 
