mirror of
https://github.com/EttusResearch/uhd.git
synced 2026-10-25 17:36:10 +00:00
host: test: Add HBX radio block test
To be similar as ZBX and FBX, this adds a radio block test that checks for setting frequency, the gain setting (via API and via properties), gain profiles, power API and the IQ_DC compensation value processing.
This commit is contained in:
parent
0455399ad8
commit
1fa8b10720
@ -537,6 +537,55 @@ IF(ENABLE_X400)
|
||||
INCLUDE_DIRS ${UHD_SOURCE_DIR}/lib/deps/rpclib/include
|
||||
INCLUDE_DIRS ${UHD_SOURCE_DIR}/lib/deps/flatbuffers/include
|
||||
)
|
||||
|
||||
UHD_ADD_RFNOC_BLOCK_NONAPI_TEST(
|
||||
TARGET hbx_radio_block_test.cpp
|
||||
EXTRA_SOURCES
|
||||
${UHD_SOURCE_DIR}/lib/usrp/common/lmx2572.cpp
|
||||
${UHD_SOURCE_DIR}/lib/usrp/common/ltc5594.cpp
|
||||
${UHD_SOURCE_DIR}/lib/usrp/common/admv1320.cpp
|
||||
${UHD_SOURCE_DIR}/lib/usrp/common/admv1420.cpp
|
||||
${UHD_SOURCE_DIR}/lib/usrp/common/pwr_cal_mgr.cpp
|
||||
${UHD_SOURCE_DIR}/lib/usrp/x400/x400_gpio_control.cpp
|
||||
${UHD_SOURCE_DIR}/lib/usrp/x400/x400_radio_control.cpp
|
||||
${UHD_SOURCE_DIR}/lib/usrp/x400/x400_rfdc_control.cpp
|
||||
${UHD_SOURCE_DIR}/lib/usrp/x400/adc_self_calibration.cpp
|
||||
${UHD_SOURCE_DIR}/lib/rfnoc/complex_gain_iface.cpp
|
||||
${UHD_SOURCE_DIR}/lib/rfnoc/radio_control_impl.cpp
|
||||
${UHD_SOURCE_DIR}/lib/rfnoc/rf_control/gain_profile.cpp
|
||||
${UHD_SOURCE_DIR}/lib/usrp/mpmd/mpmd_mb_controller.cpp
|
||||
${UHD_SOURCE_DIR}/lib/usrp/cores/complex_gain_3000.cpp
|
||||
${UHD_SOURCE_DIR}/lib/usrp/dboard/fbx/fbx_dboard.cpp
|
||||
${UHD_SOURCE_DIR}/lib/usrp/dboard/fbx/fbx_dboard_init.cpp
|
||||
${UHD_SOURCE_DIR}/lib/usrp/dboard/fbx/fbx_expert.cpp
|
||||
${UHD_SOURCE_DIR}/lib/usrp/dboard/fbx/fbx_ctrl.cpp
|
||||
${UHD_SOURCE_DIR}/lib/usrp/dboard/hbx/hbx_dboard.cpp
|
||||
${UHD_SOURCE_DIR}/lib/usrp/dboard/hbx/hbx_dboard_init.cpp
|
||||
${UHD_SOURCE_DIR}/lib/usrp/dboard/hbx/hbx_lo_ctrl.cpp
|
||||
${UHD_SOURCE_DIR}/lib/usrp/dboard/hbx/hbx_demod_ctrl.cpp
|
||||
${UHD_SOURCE_DIR}/lib/usrp/dboard/hbx/hbx_admv1320_ctrl.cpp
|
||||
${UHD_SOURCE_DIR}/lib/usrp/dboard/hbx/hbx_admv1420_ctrl.cpp
|
||||
${UHD_SOURCE_DIR}/lib/usrp/dboard/hbx/hbx_lo_pd.cpp
|
||||
${UHD_SOURCE_DIR}/lib/usrp/dboard/hbx/hbx_expert.cpp
|
||||
${UHD_SOURCE_DIR}/lib/usrp/dboard/hbx/hbx_cpld_ctrl.cpp
|
||||
${UHD_SOURCE_DIR}/lib/usrp/common/apply_corrections.cpp
|
||||
${UHD_SOURCE_DIR}/lib/usrp/dboard/zbx/zbx_dboard.cpp
|
||||
${UHD_SOURCE_DIR}/lib/usrp/dboard/zbx/zbx_dboard_init.cpp
|
||||
${UHD_SOURCE_DIR}/lib/usrp/dboard/zbx/zbx_lo_ctrl.cpp
|
||||
${UHD_SOURCE_DIR}/lib/usrp/dboard/zbx/zbx_cpld_ctrl.cpp
|
||||
${UHD_SOURCE_DIR}/lib/usrp/dboard/zbx/zbx_expert.cpp
|
||||
${UHD_SOURCE_DIR}/lib/features/discoverable_feature_registry.cpp
|
||||
${UHD_SOURCE_DIR}/lib/usrp/cores/gpio_atr_3000.cpp
|
||||
${UHD_SOURCE_DIR}/lib/usrp/cores/spi_core_4000.cpp
|
||||
${UHD_SOURCE_DIR}/lib/usrp/x400/x400_internal_sync.cpp
|
||||
${UHD_SOURCE_DIR}/lib/utils/prefs.cpp
|
||||
${UHD_SOURCE_DIR}/lib/utils/pathslib.cpp
|
||||
${UHD_SOURCE_DIR}/lib/utils/paths.cpp
|
||||
${UHD_SOURCE_DIR}/lib/utils/config_parser.cpp
|
||||
$<TARGET_OBJECTS:uhd_rpclib>
|
||||
INCLUDE_DIRS ${UHD_SOURCE_DIR}/lib/deps/rpclib/include
|
||||
INCLUDE_DIRS ${UHD_SOURCE_DIR}/lib/deps/flatbuffers/include
|
||||
)
|
||||
endif(NOT APPLE)
|
||||
|
||||
UHD_ADD_NONAPI_TEST(
|
||||
|
||||
565
host/tests/rfnoc_block_tests/hbx_radio_block_test.cpp
Normal file
565
host/tests/rfnoc_block_tests/hbx_radio_block_test.cpp
Normal file
@ -0,0 +1,565 @@
|
||||
//
|
||||
// Copyright 2026 Ettus Research, a National Instruments Brand
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
|
||||
#include "hbx_radio_mock.hpp"
|
||||
#include "x4xx_hbx_mpm_mock.hpp"
|
||||
#include <uhd/cal/database.hpp>
|
||||
#include <uhd/cal/pwr_cal.hpp>
|
||||
#include <uhd/rfnoc/detail/graph.hpp>
|
||||
#include <uhd/rfnoc/mock_block.hpp>
|
||||
#include <uhd/rfnoc/mock_nodes.hpp>
|
||||
#include <uhd/types/iq_dc_cal_coeffs.hpp>
|
||||
#include <uhd/utils/log.hpp>
|
||||
#include <uhd/utils/math.hpp>
|
||||
#include <uhd/utils/paths.hpp>
|
||||
#include <boost/test/unit_test.hpp>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <thread>
|
||||
|
||||
using namespace uhd;
|
||||
using namespace uhd::rfnoc;
|
||||
using namespace uhd::rfnoc::test;
|
||||
using namespace std::chrono_literals;
|
||||
using namespace uhd::usrp::hbx;
|
||||
using namespace uhd::experts;
|
||||
|
||||
namespace {
|
||||
|
||||
std::string _sanitize_antenna_name(std::string antenna)
|
||||
{
|
||||
std::replace(antenna.begin(), antenna.end(), '/', '+');
|
||||
std::transform(antenna.begin(), antenna.end(), antenna.begin(), [](unsigned char c) {
|
||||
return static_cast<char>(std::tolower(c));
|
||||
});
|
||||
return antenna;
|
||||
}
|
||||
|
||||
void _seed_hbx_mock_power_cal_data(x400_hbx_radio_fixture& fixture)
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
static bool seeded = false;
|
||||
if (seeded) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fs::path cal_path = fs::path(uhd::get_tmp_path()) / "UHD_HBX_MOCK_CAL";
|
||||
std::error_code ec;
|
||||
fs::create_directories(cal_path, ec);
|
||||
if (ec) {
|
||||
BOOST_FAIL("Failed to create temporary cal path: " << cal_path.string());
|
||||
}
|
||||
|
||||
#ifdef UHD_PLATFORM_WIN32
|
||||
const std::string putenv_str = std::string("UHD_CAL_DATA_PATH=") + cal_path.string();
|
||||
_putenv(putenv_str.c_str());
|
||||
#else
|
||||
setenv("UHD_CAL_DATA_PATH", cal_path.string().c_str(), 1);
|
||||
#endif
|
||||
|
||||
const std::string serial = "HBX_MOCK_NO_CAL#HBX";
|
||||
const std::string tx_key =
|
||||
"x4xx_pwr_hbx_tx_0_"
|
||||
+ _sanitize_antenna_name(fixture.test_radio->get_tx_antenna(0));
|
||||
const std::string rx_key =
|
||||
"x4xx_pwr_hbx_rx_0_"
|
||||
+ _sanitize_antenna_name(fixture.test_radio->get_rx_antenna(0));
|
||||
|
||||
const auto make_cal_blob = [serial]() {
|
||||
auto cal_data =
|
||||
uhd::usrp::cal::pwr_cal::make("HBX mock power cal", serial, 0x12345678);
|
||||
std::map<double, double> gain_to_power;
|
||||
for (int gain = 0; gain <= 80; gain++) {
|
||||
gain_to_power[static_cast<double>(gain)] = static_cast<double>(gain) - 60.0;
|
||||
}
|
||||
for (const double freq : {1e9, 3e9, 6e9, 10e9}) {
|
||||
cal_data->add_power_table(gain_to_power, -60.0, 20.0, freq);
|
||||
}
|
||||
return cal_data->serialize();
|
||||
};
|
||||
|
||||
uhd::usrp::cal::database::write_cal_data(tx_key, serial, make_cal_blob());
|
||||
uhd::usrp::cal::database::write_cal_data(rx_key, serial, make_cal_blob());
|
||||
seeded = true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
BOOST_FIXTURE_TEST_CASE(hbx_api_freq_tx_test, x400_hbx_radio_fixture)
|
||||
{
|
||||
const std::string log = "HBX_API_TX_FREQUENCY_TEST";
|
||||
const double ep = 10;
|
||||
// HBX supports 10 MHz to 20 GHz
|
||||
uhd::freq_range_t hbx_freq(HBX_MIN_FREQ, HBX_MAX_FREQ, 500e6);
|
||||
|
||||
// HBX has only 1 channel
|
||||
for (const size_t chan : {0}) {
|
||||
UHD_LOG_INFO(log, "BEGIN TEST: tx" << chan << " FREQ CHANGE (SET->RETURN)\n");
|
||||
for (double iter = hbx_freq.start(); iter <= hbx_freq.stop();
|
||||
iter += hbx_freq.step()) {
|
||||
UHD_LOG_INFO(log, "Testing freq: " << iter);
|
||||
|
||||
const double freq = test_radio->set_tx_frequency(iter, chan);
|
||||
BOOST_REQUIRE(abs(iter - freq) < ep);
|
||||
}
|
||||
|
||||
UHD_LOG_INFO(log, "BEGIN TEST: tx" << chan << " FREQ CHANGE (SET->GET)\n");
|
||||
for (double iter = hbx_freq.start(); iter <= hbx_freq.stop();
|
||||
iter += hbx_freq.step()) {
|
||||
UHD_LOG_INFO(log, "Testing freq: " << iter);
|
||||
|
||||
test_radio->set_tx_frequency(iter, chan);
|
||||
const double freq = test_radio->get_tx_frequency(chan);
|
||||
BOOST_REQUIRE(abs(iter - freq) < ep);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_FIXTURE_TEST_CASE(hbx_api_freq_rx_test, x400_hbx_radio_fixture)
|
||||
{
|
||||
const std::string log = "HBX_API_RX_FREQUENCY_TEST";
|
||||
const double ep = 10;
|
||||
// HBX supports 10 MHz to 20 GHz
|
||||
uhd::freq_range_t hbx_freq(HBX_MIN_FREQ, HBX_MAX_FREQ, 500e6);
|
||||
|
||||
// HBX has only 1 channel
|
||||
for (const size_t chan : {0}) {
|
||||
UHD_LOG_INFO(log, "BEGIN TEST: rx" << chan << " FREQ CHANGE (SET->RETURN)\n");
|
||||
for (double iter = hbx_freq.start(); iter <= hbx_freq.stop();
|
||||
iter += hbx_freq.step()) {
|
||||
UHD_LOG_INFO(log, "Testing freq: " << iter);
|
||||
|
||||
const double freq = test_radio->set_rx_frequency(iter, chan);
|
||||
BOOST_REQUIRE(abs(iter - freq) < ep);
|
||||
}
|
||||
UHD_LOG_INFO(log, "BEGIN TEST: rx" << chan << " FREQ CHANGE (SET->GET)\n");
|
||||
for (double iter = hbx_freq.start(); iter <= hbx_freq.stop();
|
||||
iter += hbx_freq.step()) {
|
||||
UHD_LOG_INFO(log, "Testing freq: " << iter);
|
||||
|
||||
test_radio->set_rx_frequency(iter, chan);
|
||||
const double freq = test_radio->get_rx_frequency(chan);
|
||||
BOOST_REQUIRE(abs(iter - freq) < ep);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_FIXTURE_TEST_CASE(hbx_frequency_test, x400_hbx_radio_fixture)
|
||||
{
|
||||
auto tree = test_radio->get_tree();
|
||||
const std::string log = "HBX_FREQUENCY_TEST";
|
||||
const double ep = 10;
|
||||
// HBX supports 10 MHz to 20 GHz
|
||||
uhd::freq_range_t hbx_freq(HBX_MIN_FREQ, HBX_MAX_FREQ, 500e6);
|
||||
|
||||
for (const auto& fe_path : {
|
||||
fs_path("dboard/tx_frontends/0"),
|
||||
fs_path("dboard/rx_frontends/0"),
|
||||
}) {
|
||||
UHD_LOG_INFO(log, "BEGIN TEST: " << fe_path << " FREQ CHANGE\n");
|
||||
for (double iter = hbx_freq.start(); iter <= hbx_freq.stop();
|
||||
iter += hbx_freq.step()) {
|
||||
UHD_LOG_INFO(log, "Testing freq: " << iter);
|
||||
|
||||
tree->access<double>(fe_path / "freq").set(iter);
|
||||
|
||||
const double ret_value = tree->access<double>(fe_path / "freq").get();
|
||||
|
||||
BOOST_REQUIRE(abs(iter - ret_value) < ep);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_FIXTURE_TEST_CASE(hbx_api_tx_gain_test, x400_hbx_radio_fixture)
|
||||
{
|
||||
auto tree = test_radio->get_tree();
|
||||
const std::string log = "HBX_API_TX_GAIN_TEST";
|
||||
const auto gain_range = test_radio->get_tx_gain_range(0);
|
||||
uhd::gain_range_t hbx_gain(gain_range.start(), gain_range.stop(), gain_range.step());
|
||||
|
||||
for (const size_t chan : {0}) {
|
||||
UHD_LOG_INFO(log, "BEGIN TEST: tx" << chan << " GAIN CHANGE (SET->RETURN)\n");
|
||||
for (double iter = hbx_gain.start(); iter <= hbx_gain.stop();
|
||||
iter += hbx_gain.step()) {
|
||||
UHD_LOG_INFO(log, "Testing gain: " << iter);
|
||||
const double ret_gain = test_radio->set_tx_gain(iter, chan);
|
||||
BOOST_CHECK_EQUAL(iter, ret_gain);
|
||||
}
|
||||
|
||||
UHD_LOG_INFO(log, "BEGIN TEST: tx" << chan << " GAIN CHANGE (SET->GET)\n");
|
||||
for (double iter = hbx_gain.start(); iter <= hbx_gain.stop();
|
||||
iter += hbx_gain.step()) {
|
||||
UHD_LOG_INFO(log, "Testing gain: " << iter);
|
||||
test_radio->set_tx_gain(iter, chan);
|
||||
const double ret_gain = test_radio->get_tx_gain(chan);
|
||||
BOOST_CHECK_EQUAL(iter, ret_gain);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_FIXTURE_TEST_CASE(hbx_api_rx_gain_test, x400_hbx_radio_fixture)
|
||||
{
|
||||
auto tree = test_radio->get_tree();
|
||||
const std::string log = "HBX_API_RX_GAIN_TEST";
|
||||
const auto gain_range = test_radio->get_rx_gain_range(0);
|
||||
uhd::gain_range_t hbx_gain(gain_range.start(), gain_range.stop(), gain_range.step());
|
||||
|
||||
for (const size_t chan : {0}) {
|
||||
UHD_LOG_INFO(log, "BEGIN TEST: rx" << chan << " GAIN CHANGE (SET->RETURN)\n");
|
||||
for (double iter = hbx_gain.start(); iter <= hbx_gain.stop();
|
||||
iter += hbx_gain.step()) {
|
||||
UHD_LOG_INFO(log, "Testing gain: " << iter);
|
||||
const double ret_gain = test_radio->set_rx_gain(iter, chan);
|
||||
BOOST_CHECK_EQUAL(iter, ret_gain);
|
||||
}
|
||||
|
||||
UHD_LOG_INFO(log, "BEGIN TEST: rx" << chan << " GAIN CHANGE (SET->GET)\n");
|
||||
for (double iter = hbx_gain.start(); iter <= hbx_gain.stop();
|
||||
iter += hbx_gain.step()) {
|
||||
UHD_LOG_INFO(log, "Testing gain: " << iter);
|
||||
test_radio->set_rx_gain(iter, chan);
|
||||
const double ret_gain = test_radio->get_rx_gain(chan);
|
||||
BOOST_CHECK_EQUAL(iter, ret_gain);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_FIXTURE_TEST_CASE(hbx_tx_power_api, x400_hbx_radio_fixture)
|
||||
{
|
||||
_seed_hbx_mock_power_cal_data(*this);
|
||||
|
||||
const std::string log = "HBX_TX_POWER_TRACKING_TEST";
|
||||
const size_t chan = 0;
|
||||
const auto gain_range = test_radio->get_tx_gain_range(chan);
|
||||
const double tx_given_gain = (gain_range.start() + gain_range.stop()) / 2.0;
|
||||
constexpr double tx_given_power = -30.0;
|
||||
|
||||
UHD_LOG_INFO(log, "BEGIN TEST: tx" << chan << " POWER TRACKING API\n");
|
||||
BOOST_REQUIRE_EQUAL(test_radio->set_tx_gain(tx_given_gain, chan), tx_given_gain);
|
||||
|
||||
for (const double freq : {1e9, 3e9, 6e9, 10e9}) {
|
||||
BOOST_REQUIRE_NO_THROW(test_radio->set_tx_power_reference(tx_given_power, chan));
|
||||
|
||||
test_radio->set_tx_frequency(freq, chan);
|
||||
|
||||
const double actual_power = test_radio->get_tx_power_reference(chan);
|
||||
const double pow_diff = std::abs(tx_given_power - actual_power);
|
||||
BOOST_CHECK_MESSAGE(pow_diff < 3.0,
|
||||
"TX power differential is too large ("
|
||||
<< pow_diff << "): expected close to " << tx_given_power << " actual "
|
||||
<< actual_power << " frequency " << (freq / 1e6) << " MHz");
|
||||
|
||||
BOOST_REQUIRE_EQUAL(test_radio->set_tx_gain(tx_given_gain, chan), tx_given_gain);
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_FIXTURE_TEST_CASE(hbx_rx_power_api, x400_hbx_radio_fixture)
|
||||
{
|
||||
_seed_hbx_mock_power_cal_data(*this);
|
||||
|
||||
const std::string log = "HBX_RX_POWER_TRACKING_TEST";
|
||||
const size_t chan = 0;
|
||||
const auto gain_range = test_radio->get_rx_gain_range(chan);
|
||||
const double rx_given_gain = (gain_range.start() + gain_range.stop()) / 2.0;
|
||||
constexpr double rx_given_power = -30.0;
|
||||
|
||||
UHD_LOG_INFO(log, "BEGIN TEST: rx" << chan << " POWER TRACKING API\n");
|
||||
BOOST_REQUIRE_EQUAL(test_radio->set_rx_gain(rx_given_gain, chan), rx_given_gain);
|
||||
|
||||
for (const double freq : {1e9, 3e9, 6e9, 10e9}) {
|
||||
BOOST_REQUIRE_NO_THROW(test_radio->set_rx_power_reference(rx_given_power, chan));
|
||||
|
||||
test_radio->set_rx_frequency(freq, chan);
|
||||
|
||||
const double actual_power = test_radio->get_rx_power_reference(chan);
|
||||
const double pow_diff = std::abs(rx_given_power - actual_power);
|
||||
BOOST_CHECK_MESSAGE(pow_diff < 3.0,
|
||||
"RX power differential is too large ("
|
||||
<< pow_diff << "): expected close to " << rx_given_power << " actual "
|
||||
<< actual_power << " frequency " << (freq / 1e6) << " MHz");
|
||||
|
||||
BOOST_REQUIRE_EQUAL(test_radio->set_rx_gain(rx_given_gain, chan), rx_given_gain);
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_FIXTURE_TEST_CASE(hbx_api_tx_gain_stage_test, x400_hbx_radio_fixture)
|
||||
{
|
||||
const std::string log = "HBX_API_TX_GAIN_STAGE_TEST";
|
||||
|
||||
for (const size_t chan : {0}) {
|
||||
test_radio->set_tx_gain_profile(HBX_GAIN_PROFILE_MANUAL, chan);
|
||||
|
||||
UHD_LOG_INFO(
|
||||
log, "BEGIN TEST: tx" << chan << " GAIN STAGE CHANGE (SET->RETURN)\n");
|
||||
for (const auto& gain_stage : HBX_TX_GAIN_STAGES_MANUAL) {
|
||||
const auto gain_range = test_radio->get_tx_gain_range(gain_stage, chan);
|
||||
const double prime_gain = (gain_range.start() == gain_range.stop())
|
||||
? gain_range.start()
|
||||
: gain_range.stop();
|
||||
test_radio->set_tx_gain(prime_gain, gain_stage, chan);
|
||||
if (gain_range.step() == 0.0) {
|
||||
const double ret_gain =
|
||||
test_radio->set_tx_gain(gain_range.start(), gain_stage, chan);
|
||||
BOOST_CHECK_EQUAL(gain_range.start(), ret_gain);
|
||||
continue;
|
||||
}
|
||||
for (double iter = gain_range.start(); iter <= gain_range.stop();
|
||||
iter += gain_range.step()) {
|
||||
UHD_LOG_INFO(log, "Testing " << gain_stage << ": " << iter);
|
||||
const double ret_gain = test_radio->set_tx_gain(iter, gain_stage, chan);
|
||||
BOOST_CHECK_EQUAL(iter, ret_gain);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_FIXTURE_TEST_CASE(hbx_api_rx_gain_stage_test, x400_hbx_radio_fixture)
|
||||
{
|
||||
const std::string log = "HBX_API_RX_GAIN_STAGE_TEST";
|
||||
|
||||
for (const size_t chan : {0}) {
|
||||
test_radio->set_rx_gain_profile(HBX_GAIN_PROFILE_MANUAL, chan);
|
||||
|
||||
UHD_LOG_INFO(
|
||||
log, "BEGIN TEST: rx" << chan << " GAIN STAGE CHANGE (SET->RETURN)\n");
|
||||
for (const auto& gain_stage : HBX_RX_GAIN_STAGES_MANUAL) {
|
||||
const auto gain_range = test_radio->get_rx_gain_range(gain_stage, chan);
|
||||
const double prime_gain = (gain_range.start() == gain_range.stop())
|
||||
? gain_range.start()
|
||||
: gain_range.stop();
|
||||
test_radio->set_rx_gain(prime_gain, gain_stage, chan);
|
||||
if (gain_range.step() == 0.0) {
|
||||
const double ret_gain =
|
||||
test_radio->set_rx_gain(gain_range.start(), gain_stage, chan);
|
||||
BOOST_CHECK_EQUAL(gain_range.start(), ret_gain);
|
||||
continue;
|
||||
}
|
||||
for (double iter = gain_range.start(); iter <= gain_range.stop();
|
||||
iter += gain_range.step()) {
|
||||
UHD_LOG_INFO(log, "Testing " << gain_stage << ": " << iter);
|
||||
const double ret_gain = test_radio->set_rx_gain(iter, gain_stage, chan);
|
||||
BOOST_CHECK_EQUAL(iter, ret_gain);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_FIXTURE_TEST_CASE(hbx_tx_gain_test, x400_hbx_radio_fixture)
|
||||
{
|
||||
auto tree = test_radio->get_tree();
|
||||
const std::string log = "HBX_TX_GAIN_TEST";
|
||||
const auto gain_range = test_radio->get_tx_gain_range(0);
|
||||
uhd::gain_range_t hbx_gain(gain_range.start(), gain_range.stop(), gain_range.step());
|
||||
|
||||
for (const auto& fe_path : {fs_path("dboard/tx_frontends/0")}) {
|
||||
UHD_LOG_INFO(log, "BEGIN TEST: " << fe_path << " GAIN CHANGE\n");
|
||||
for (double iter = hbx_gain.start(); iter <= hbx_gain.stop();
|
||||
iter += hbx_gain.step()) {
|
||||
UHD_LOG_INFO(log, "Testing gain: " << iter);
|
||||
const auto gain_path = fe_path / "gains" / HBX_GAIN_STAGE_ALL / "value";
|
||||
tree->access<double>(gain_path).set(iter);
|
||||
const double ret_gain = tree->access<double>(gain_path).get();
|
||||
BOOST_CHECK_EQUAL(iter, ret_gain);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_FIXTURE_TEST_CASE(hbx_rx_gain_test, x400_hbx_radio_fixture)
|
||||
{
|
||||
auto tree = test_radio->get_tree();
|
||||
const std::string log = "HBX_RX_GAIN_TEST";
|
||||
const auto gain_range = test_radio->get_rx_gain_range(0);
|
||||
uhd::gain_range_t hbx_gain(gain_range.start(), gain_range.stop(), gain_range.step());
|
||||
|
||||
for (const auto& fe_path : {fs_path("dboard/rx_frontends/0")}) {
|
||||
UHD_LOG_INFO(log, "BEGIN TEST: " << fe_path << " GAIN CHANGE\n");
|
||||
for (double iter = hbx_gain.start(); iter <= hbx_gain.stop();
|
||||
iter += hbx_gain.step()) {
|
||||
UHD_LOG_INFO(log, "Testing gain: " << iter);
|
||||
const auto gain_path = fe_path / "gains" / HBX_GAIN_STAGE_ALL / "value";
|
||||
tree->access<double>(gain_path).set(iter);
|
||||
const double ret_gain = tree->access<double>(gain_path).get();
|
||||
BOOST_CHECK_EQUAL(iter, ret_gain);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_FIXTURE_TEST_CASE(hbx_rx_antenna_test, x400_hbx_radio_fixture)
|
||||
{
|
||||
const std::string log = "HBX_RX_ANTENNA_TEST";
|
||||
const size_t chan = 0; // HBX has only 1 channel
|
||||
|
||||
UHD_LOG_INFO(log, "BEGIN TEST: get available RX antennas\n");
|
||||
auto available_antennas = test_radio->get_rx_antennas(chan);
|
||||
UHD_LOG_INFO(log, "Available RX antennas: ");
|
||||
for (const auto& ant : available_antennas) {
|
||||
UHD_LOG_INFO(log, " " << ant);
|
||||
}
|
||||
|
||||
// Test that setting and getting the antennas works
|
||||
for (const std::string& ant : available_antennas) {
|
||||
UHD_LOG_INFO(log, "Testing antenna: " << ant);
|
||||
test_radio->set_rx_antenna(ant, chan);
|
||||
BOOST_REQUIRE_EQUAL(ant, test_radio->get_rx_antenna(chan));
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_FIXTURE_TEST_CASE(hbx_tx_antenna_test, x400_hbx_radio_fixture)
|
||||
{
|
||||
const std::string log = "HBX_TX_ANTENNA_TEST";
|
||||
const size_t chan = 0; // HBX has only 1 channel
|
||||
|
||||
UHD_LOG_INFO(log, "BEGIN TEST: get available TX antennas\n");
|
||||
auto available_antennas = test_radio->get_tx_antennas(chan);
|
||||
UHD_LOG_INFO(log, "Available TX antennas: ");
|
||||
for (const auto& ant : available_antennas) {
|
||||
UHD_LOG_INFO(log, " " << ant);
|
||||
}
|
||||
|
||||
auto prev_ant = test_radio->get_tx_antenna(chan);
|
||||
for (const std::string& ant : available_antennas) {
|
||||
UHD_LOG_INFO(log, "Testing antenna: " << ant);
|
||||
test_radio->set_tx_antenna(ant, chan);
|
||||
BOOST_REQUIRE_EQUAL(ant, test_radio->get_tx_antenna(chan));
|
||||
prev_ant = ant;
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************
|
||||
* RFNoC Graph Test
|
||||
*
|
||||
* This test case ensures that the Radio Block can be added to an RFNoC graph.
|
||||
*****************************************************************************/
|
||||
BOOST_FIXTURE_TEST_CASE(hbx_radio_test_graph, x400_hbx_radio_fixture)
|
||||
{
|
||||
detail::graph_t graph{};
|
||||
detail::graph_t::graph_edge_t edge_port_info0;
|
||||
edge_port_info0.src_port = 0;
|
||||
edge_port_info0.dst_port = 0;
|
||||
edge_port_info0.edge = detail::graph_t::graph_edge_t::DYNAMIC;
|
||||
|
||||
mock_radio_node_t mock_radio_block{0};
|
||||
mock_terminator_t mock_sink_term(1, {}, "MOCK_SINK");
|
||||
mock_terminator_t mock_source_term(1, {}, "MOCK_SOURCE");
|
||||
|
||||
UHD_LOG_INFO("TEST", "Priming mock block properties");
|
||||
node_accessor.init_props(&mock_radio_block);
|
||||
mock_source_term.set_edge_property<std::string>(
|
||||
"type", "sc16", {res_source_info::OUTPUT_EDGE, 0});
|
||||
mock_sink_term.set_edge_property<std::string>(
|
||||
"type", "sc16", {res_source_info::INPUT_EDGE, 0});
|
||||
|
||||
UHD_LOG_INFO("TEST", "Creating graph...");
|
||||
graph.connect(&mock_source_term, test_radio.get(), edge_port_info0);
|
||||
graph.connect(test_radio.get(), &mock_sink_term, edge_port_info0);
|
||||
UHD_LOG_INFO("TEST", "Committing graph...");
|
||||
graph.commit();
|
||||
UHD_LOG_INFO("TEST", "Commit complete.");
|
||||
}
|
||||
|
||||
// Helper lambdas that replicate the encoding math from hbx_iq_dc_correction_expert.
|
||||
// coeff_to_fixed: convert a double coefficient to the COEFFS_FRAC_BITS fixed-point
|
||||
// representation used by the RF core (COEFF_WIDTH bits wide).
|
||||
// iq_to_dc_offset: pack I/Q DC offset into a single 32-bit register value.
|
||||
// Both lambdas match the formulas in hbx_expert.cpp exactly.
|
||||
//
|
||||
// Note: _rfdc_dc_conv_offset is {0,0} for TX and {0,0} for RX in the mock
|
||||
// (get_cal_coefs returns a single zero, giving i_avg=q_avg=0.0).
|
||||
namespace {
|
||||
constexpr auto coeff_to_fixed = [](double c) -> uint32_t {
|
||||
return static_cast<uint32_t>(c * (1u << COEFFS_FRAC_BITS))
|
||||
& ((1u << COEFF_WIDTH) - 1u);
|
||||
};
|
||||
constexpr auto iq_to_dc_offset = [](std::complex<double> dc) -> uint32_t {
|
||||
// rfdc_dc_conv_offset is {0,0} in the mock, so no subtraction needed.
|
||||
const int32_t i_s = static_cast<int32_t>(std::lrint(dc.real() * (1 << 15)));
|
||||
const int32_t q_s = static_cast<int32_t>(std::lrint(dc.imag() * (1 << 15)));
|
||||
return (static_cast<uint32_t>(static_cast<uint16_t>(q_s)) << 16)
|
||||
| static_cast<uint32_t>(static_cast<uint16_t>(i_s));
|
||||
};
|
||||
} // namespace
|
||||
|
||||
BOOST_FIXTURE_TEST_CASE(hbx_tx_iq_dc_compensation_test, x400_hbx_radio_fixture)
|
||||
{
|
||||
// Prime the coerced TX frequency to above HBX_SWITCH_FREQ so the expert
|
||||
// uses the user-supplied coefficients instead of IQ_DC_DEFAULT_VALUES.
|
||||
test_radio->set_tx_frequency(8e9, 0);
|
||||
|
||||
// Coefficients chosen so all expected register values are exact powers of 2,
|
||||
// making the assertions easy to reason about.
|
||||
uhd::iq_dc_cal_coeffs_t test_coeffs;
|
||||
test_coeffs.scaling_coeff = 0.5; // iinline = 0x400000
|
||||
test_coeffs.coeffs = {{0.25, 0.125}}; // icross = 0x200000, qinline = 0x100000
|
||||
test_coeffs.group_delay = 4.0; // group_delay register = 4
|
||||
test_coeffs.dc_offset = {0.0, 0.0}; // DC register = 0
|
||||
|
||||
// Writing the coefficients into the property tree immediately fires the
|
||||
// IQ/DC correction expert (AUTO_RESOLVE_ON_WRITE), which encodes them and
|
||||
// pokes the RF core registers.
|
||||
test_radio->get_tree()
|
||||
->access<uhd::iq_dc_cal_coeffs_t>("dboard/tx_frontends/0/iq_balance/coeffs/value")
|
||||
.set(test_coeffs);
|
||||
|
||||
// Verify that each register received the correctly encoded value.
|
||||
// The loop writes coefficients in reverse index order, so idx=0 is written last;
|
||||
// write_log therefore holds the encoding of coeffs[0] for ICROSS and QINLINE.
|
||||
const uint32_t iq_base = radio_control_impl::regmap::PERIPH_BASE + RF_CORE_ADDR_OFFSET
|
||||
+ IQ_IMPAIRMENTS_TX_OFFSET;
|
||||
const uint32_t dc_base =
|
||||
radio_control_impl::regmap::PERIPH_BASE + RF_CORE_ADDR_OFFSET + DC_TX_OFFSET;
|
||||
const auto& wl = reg_iface->write_log;
|
||||
|
||||
BOOST_CHECK_EQUAL(wl.at(iq_base + IINLINE_COEFF_REG_OFFSET),
|
||||
coeff_to_fixed(test_coeffs.scaling_coeff));
|
||||
BOOST_CHECK_EQUAL(wl.at(iq_base + ICROSS_COEFF_REG_OFFSET),
|
||||
coeff_to_fixed(test_coeffs.coeffs[0].real()));
|
||||
BOOST_CHECK_EQUAL(wl.at(iq_base + QINLINE_COEFF_REG_OFFSET),
|
||||
coeff_to_fixed(test_coeffs.coeffs[0].imag()));
|
||||
BOOST_CHECK_EQUAL(wl.at(iq_base + GROUP_DELAY_REG_OFFSET),
|
||||
static_cast<uint32_t>(test_coeffs.group_delay));
|
||||
BOOST_CHECK_EQUAL(
|
||||
wl.at(dc_base + DC_VALUE_OFFSET), iq_to_dc_offset(test_coeffs.dc_offset));
|
||||
}
|
||||
|
||||
BOOST_FIXTURE_TEST_CASE(hbx_rx_iq_dc_compensation_test, x400_hbx_radio_fixture)
|
||||
{
|
||||
test_radio->set_rx_frequency(8e9, 0);
|
||||
|
||||
uhd::iq_dc_cal_coeffs_t test_coeffs;
|
||||
test_coeffs.scaling_coeff = 0.5;
|
||||
test_coeffs.coeffs = {{0.25, 0.125}};
|
||||
test_coeffs.group_delay = 4.0;
|
||||
test_coeffs.dc_offset = {0.0, 0.0};
|
||||
|
||||
test_radio->get_tree()
|
||||
->access<uhd::iq_dc_cal_coeffs_t>("dboard/rx_frontends/0/iq_balance/coeffs/value")
|
||||
.set(test_coeffs);
|
||||
|
||||
const uint32_t iq_base = radio_control_impl::regmap::PERIPH_BASE + RF_CORE_ADDR_OFFSET
|
||||
+ IQ_IMPAIRMENTS_RX_OFFSET;
|
||||
const uint32_t dc_base =
|
||||
radio_control_impl::regmap::PERIPH_BASE + RF_CORE_ADDR_OFFSET + DC_RX_OFFSET;
|
||||
const auto& wl = reg_iface->write_log;
|
||||
|
||||
BOOST_CHECK_EQUAL(wl.at(iq_base + IINLINE_COEFF_REG_OFFSET),
|
||||
coeff_to_fixed(test_coeffs.scaling_coeff));
|
||||
BOOST_CHECK_EQUAL(wl.at(iq_base + ICROSS_COEFF_REG_OFFSET),
|
||||
coeff_to_fixed(test_coeffs.coeffs[0].real()));
|
||||
BOOST_CHECK_EQUAL(wl.at(iq_base + QINLINE_COEFF_REG_OFFSET),
|
||||
coeff_to_fixed(test_coeffs.coeffs[0].imag()));
|
||||
BOOST_CHECK_EQUAL(wl.at(iq_base + GROUP_DELAY_REG_OFFSET),
|
||||
static_cast<uint32_t>(test_coeffs.group_delay));
|
||||
BOOST_CHECK_EQUAL(
|
||||
wl.at(dc_base + DC_VALUE_OFFSET), iq_to_dc_offset(test_coeffs.dc_offset));
|
||||
}
|
||||
304
host/tests/rfnoc_block_tests/hbx_radio_mock.hpp
Normal file
304
host/tests/rfnoc_block_tests/hbx_radio_mock.hpp
Normal file
@ -0,0 +1,304 @@
|
||||
//
|
||||
// Copyright 2026 Ettus Research, a National Instruments Brand
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../../lib/usrp/x400/x400_radio_control.hpp"
|
||||
#include "x4xx_hbx_mpm_mock.hpp"
|
||||
#include <uhd/rfnoc/mock_block.hpp>
|
||||
#include <uhd/rfnoc/node_accessor.hpp>
|
||||
#include <uhd/utils/log.hpp>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
|
||||
using namespace uhd;
|
||||
using namespace uhd::rfnoc;
|
||||
using namespace std::chrono_literals;
|
||||
using namespace uhd::usrp::hbx;
|
||||
using namespace uhd::experts;
|
||||
|
||||
namespace {
|
||||
/* This class extends mock_reg_iface_t by adding a constructor that initializes
|
||||
* some of the read memory to contain the memory size for the radio block.
|
||||
*/
|
||||
class hbx_radio_mock_reg_iface_t : public mock_reg_iface_t
|
||||
{
|
||||
// Start address of CPLD register space
|
||||
static constexpr uint32_t cpld_offset = radio_control_impl::regmap::PERIPH_BASE;
|
||||
// Start address of RFDC control register space
|
||||
static constexpr uint32_t rfdc_offset =
|
||||
radio_control_impl::regmap::PERIPH_BASE + 0x8000;
|
||||
static constexpr uint32_t spi_offset =
|
||||
radio_control_impl::regmap::PERIPH_BASE
|
||||
+ 0xC000 /*DIO Window*/ + 0x2000 /*DIO Regmap*/;
|
||||
static constexpr uint32_t gpio_offset =
|
||||
radio_control_impl::regmap::PERIPH_BASE
|
||||
+ 0xC000 /*DIO Window*/ + 0x1000 /*GPIO Regmap*/;
|
||||
|
||||
// In HBX, SPI info register encoding is:
|
||||
// - bits [7:0] data width
|
||||
// - bits [15:8] address width
|
||||
// - bits [31:16] FIFO depth
|
||||
static constexpr uint32_t spi_info_value = 16 /* data width */
|
||||
| (8 /* address width */ << 8)
|
||||
| (1 /* fifo depth */ << 16);
|
||||
static constexpr uint32_t spi_status_ready = (1u << 31) | (1u << 30);
|
||||
|
||||
public:
|
||||
hbx_radio_mock_reg_iface_t(size_t num_channels)
|
||||
{
|
||||
for (size_t chan = 0; chan < num_channels; chan++) {
|
||||
const uint32_t reg_compat =
|
||||
radio_control_impl::regmap::REG_COMPAT_NUM
|
||||
+ chan * radio_control_impl::regmap::REG_CHAN_OFFSET;
|
||||
read_memory[reg_compat] = (radio_control_impl::MINOR_COMPAT
|
||||
| (radio_control_impl::MAJOR_COMPAT << 16));
|
||||
}
|
||||
read_memory[radio_control_impl::regmap::REG_RADIO_WIDTH] =
|
||||
(32 /* bits per sample */ << 16) | 1 /* sample per clock */;
|
||||
// Ensure that the SPI Status is always SPI_READY
|
||||
read_memory[spi_offset + 0x18] |= 1 << 24;
|
||||
|
||||
// Initialize all HBX CPLD SPI engines that are used by the HBX dboard init
|
||||
for (const uint32_t spi_info_offset :
|
||||
{0x1000u, 0x1010u, 0x1020u, 0x1030u, 0x1040u, 0x1050u, 0x1060u}) {
|
||||
read_memory[cpld_offset + spi_info_offset] = spi_info_value;
|
||||
read_memory[cpld_offset + spi_info_offset + 0x8] = spi_status_ready;
|
||||
_lo_muxout_to_lock[cpld_offset + spi_info_offset] = false;
|
||||
}
|
||||
|
||||
// Setup the GPIO addresses
|
||||
read_memory[gpio_offset + 0x4] = 0;
|
||||
|
||||
// Disable the complex gain feature
|
||||
read_memory[radio_control_impl::regmap::RADIO_BASE_ADDR
|
||||
+ radio_control_impl::regmap::REG_FEATURES_PRESENT] = 0;
|
||||
|
||||
// RFDC IQ/DC correction metadata used by HBX experts.
|
||||
// Address 0x8A020 (PERIPH_BASE + 0xA020) is read during init to determine
|
||||
// the available number of IQ correction coefficients.
|
||||
// Addresses PERIPH_BASE + 0xA000 (TX) and +0xA020 (RX) are read during
|
||||
// init to determine the available number of IQ correction coefficients.
|
||||
read_memory[rfdc_offset + 0x2000] = 64;
|
||||
read_memory[rfdc_offset + 0x2020] = 64;
|
||||
}
|
||||
|
||||
void _poke_cb(uint32_t addr, uint32_t data, uhd::time_spec_t, bool) override
|
||||
{
|
||||
// Are we on the peripheral?
|
||||
if (addr >= radio_control_impl::regmap::PERIPH_BASE) {
|
||||
// handle all the periphs stuff that is not CPLD here
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
// Are we poking the RFDC controls?
|
||||
if (addr >= rfdc_offset) {
|
||||
_poke_rfdc_cb(addr, data);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle CPLD register space
|
||||
if (addr >= cpld_offset && addr < rfdc_offset) {
|
||||
if (_is_spi_setup_addr(addr)) {
|
||||
_poke_spi_setup(addr, data);
|
||||
return;
|
||||
}
|
||||
read_memory[addr] = data;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void _poke_rfdc_cb(const uint32_t addr, const uint32_t data)
|
||||
{
|
||||
read_memory[addr] |= data;
|
||||
// Track the last value written to each address so tests can verify
|
||||
// exact register contents without the OR-accumulation of read_memory.
|
||||
write_log[addr] = data;
|
||||
}
|
||||
|
||||
// Records the last value poked to each address in the RFDC register space.
|
||||
std::map<uint32_t, uint32_t> write_log;
|
||||
|
||||
void _peek_cb(uint32_t addr, uhd::time_spec_t /*time*/) override
|
||||
{
|
||||
// HBX radio code may read peripheral registers that have no explicit mock
|
||||
// behavior. Default those reads to zero instead of throwing from
|
||||
// mock_reg_iface_t.
|
||||
if (addr >= cpld_offset && read_memory.count(addr) == 0) {
|
||||
read_memory[addr] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool _is_spi_setup_addr(const uint32_t addr) const
|
||||
{
|
||||
const uint32_t offset = addr - cpld_offset;
|
||||
switch (offset) {
|
||||
case 0x1004:
|
||||
case 0x1014:
|
||||
case 0x1024:
|
||||
case 0x1034:
|
||||
case 0x1044:
|
||||
case 0x1054:
|
||||
case 0x1064:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void _poke_spi_setup(const uint32_t addr, const uint32_t data)
|
||||
{
|
||||
const uint32_t TX_LO_SPI_INFO = 0x1000;
|
||||
const uint32_t RX_LO_SPI_INFO = 0x1010;
|
||||
const uint32_t IQ_DEMOD_SPI_INFO = 0x1040;
|
||||
const uint32_t spi_base = addr - 0x4;
|
||||
const uint32_t spi_info = read_memory[spi_base];
|
||||
uint32_t data_width = spi_info & 0xFF;
|
||||
uint32_t addr_width = (spi_info >> 8) & 0xFF;
|
||||
if (data_width == 0) {
|
||||
data_width = 16;
|
||||
}
|
||||
if (addr_width == 0) {
|
||||
addr_width = 8;
|
||||
}
|
||||
|
||||
const bool start_xact = (data & (1u << 31)) != 0;
|
||||
if (!start_xact) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool is_read = (data & (1u << 30)) != 0;
|
||||
const uint32_t spi_addr = (data >> data_width) & ((1u << addr_width) - 1);
|
||||
const uint16_t spi_wdata = static_cast<uint16_t>(data & ((1u << data_width) - 1));
|
||||
|
||||
if (!is_read) {
|
||||
_spi_registers[spi_base][spi_addr] = spi_wdata;
|
||||
// LMX lock-detect emulation. Register 0 bit 2 selects lock detect mode.
|
||||
if ((spi_base == cpld_offset + TX_LO_SPI_INFO)
|
||||
|| (spi_base == cpld_offset + RX_LO_SPI_INFO)) {
|
||||
if (spi_addr == 0) {
|
||||
_lo_muxout_to_lock[spi_base] = (spi_wdata & (1u << 2)) != 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t spi_rdata = 0;
|
||||
{
|
||||
auto& spi_regs = _spi_registers[spi_base];
|
||||
const auto it = spi_regs.find(spi_addr);
|
||||
if (it != spi_regs.end()) {
|
||||
spi_rdata = it->second;
|
||||
} else if (spi_base == cpld_offset + IQ_DEMOD_SPI_INFO
|
||||
&& (spi_addr == 0x0C || spi_addr == 0x0D)) {
|
||||
// LTC5594 reset defaults for HD2IY and HD2IX.
|
||||
spi_rdata = 0x80;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_read
|
||||
&& ((spi_base == cpld_offset + TX_LO_SPI_INFO)
|
||||
|| (spi_base == cpld_offset + RX_LO_SPI_INFO))
|
||||
&& spi_addr == 125) {
|
||||
// LMX2572 ID register readback expected by initialization code.
|
||||
spi_rdata = 0x2288;
|
||||
}
|
||||
if (is_read
|
||||
&& ((spi_base == cpld_offset + TX_LO_SPI_INFO)
|
||||
|| (spi_base == cpld_offset + RX_LO_SPI_INFO))
|
||||
&& _lo_muxout_to_lock[spi_base]) {
|
||||
// LMX returns 0xFFFF when lock detect is routed to MUXOUT and PLL is locked.
|
||||
spi_rdata = 0xFFFF;
|
||||
}
|
||||
|
||||
read_memory[spi_base + 0x8] = spi_status_ready | (spi_addr << data_width)
|
||||
| (spi_rdata & ((1u << data_width) - 1));
|
||||
}
|
||||
|
||||
// SPI register model keyed by SPI base address and SPI register address
|
||||
std::map<uint32_t, std::map<uint32_t, uint16_t>> _spi_registers;
|
||||
std::map<uint32_t, bool> _lo_muxout_to_lock;
|
||||
}; // class hbx_radio_mock_reg_iface_t
|
||||
|
||||
/*
|
||||
* x400_hbx_radio_fixture is a class which is instantiated before each test
|
||||
* case is run. It sets up the block container, mock register interface,
|
||||
* and x400_radio_control object, all of which are accessible to the test
|
||||
* case. The instance of the object is destroyed at the end of each test
|
||||
* case.
|
||||
*/
|
||||
constexpr size_t DEFAULT_MTU = 8000;
|
||||
|
||||
//! Helper class to make sure we get the most logging. The logging level can be
|
||||
// overridden using a special test-specific environment variable,
|
||||
// `UHD_UNITTEST_LOG_LEVEL`.
|
||||
struct uhd_log_enabler
|
||||
{
|
||||
uhd_log_enabler(uhd::log::severity_level level)
|
||||
{
|
||||
const char* env_p = std::getenv("UHD_UNITTEST_LOG_LEVEL");
|
||||
if (env_p) {
|
||||
auto parsed_level = uhd::log::parse_log_level_from_string(env_p);
|
||||
if (parsed_level) {
|
||||
level = *parsed_level;
|
||||
} else {
|
||||
std::cout << "Unable to parse UHD_UNITTEST_LOG_LEVEL " << env_p
|
||||
<< std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "Setting log level to " << level << "..." << std::endl;
|
||||
uhd::log::set_log_level(level);
|
||||
uhd::log::set_console_level(level);
|
||||
std::this_thread::sleep_for(10ms);
|
||||
}
|
||||
};
|
||||
|
||||
struct x400_hbx_radio_fixture
|
||||
{
|
||||
x400_hbx_radio_fixture()
|
||||
: ule(uhd::log::trace) // Note: When debugging this test, either set
|
||||
// this to a lower level, or create a
|
||||
// uhd_log_enabler in the test-under-test
|
||||
, num_channels(uhd::usrp::hbx::HBX_MAX_NUM_CHANS)
|
||||
, num_input_ports(num_channels)
|
||||
, num_output_ports(num_channels)
|
||||
, reg_iface(std::make_shared<hbx_radio_mock_reg_iface_t>(num_channels))
|
||||
, rpcs(std::make_shared<uhd::test::x4xx_hbx_mock_rpc_server>(device_info))
|
||||
, mbc(std::make_shared<mpmd_mb_controller>(rpcs, device_info))
|
||||
, block_container(get_mock_block(RADIO_BLOCK,
|
||||
num_channels,
|
||||
num_channels,
|
||||
device_info,
|
||||
DEFAULT_MTU,
|
||||
X400,
|
||||
reg_iface,
|
||||
mbc))
|
||||
, test_radio(block_container.get_block<x400_radio_control_impl>())
|
||||
{
|
||||
node_accessor.init_props(test_radio.get());
|
||||
}
|
||||
|
||||
~x400_hbx_radio_fixture() {}
|
||||
|
||||
|
||||
// Must remain the first member so we make sure the log level is high
|
||||
uhd_log_enabler ule;
|
||||
const size_t num_channels;
|
||||
const size_t num_input_ports;
|
||||
const size_t num_output_ports;
|
||||
uhd::device_addr_t device_info = uhd::device_addr_t("master_clock_rate=1.25e9");
|
||||
std::shared_ptr<hbx_radio_mock_reg_iface_t> reg_iface;
|
||||
std::shared_ptr<uhd::test::x4xx_hbx_mock_rpc_server> rpcs;
|
||||
mpmd_mb_controller::sptr mbc;
|
||||
|
||||
mock_block_container block_container;
|
||||
std::shared_ptr<x400_radio_control_impl> test_radio;
|
||||
node_accessor_t node_accessor{};
|
||||
};
|
||||
} // namespace
|
||||
422
host/tests/rfnoc_block_tests/x4xx_hbx_mpm_mock.hpp
Normal file
422
host/tests/rfnoc_block_tests/x4xx_hbx_mpm_mock.hpp
Normal file
@ -0,0 +1,422 @@
|
||||
//
|
||||
// Copyright 2026 Ettus Research, a National Instruments Brand
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <uhd/types/device_addr.hpp>
|
||||
#include <uhdlib/usrp/common/rpc.hpp>
|
||||
#include <uhdlib/usrp/dboard/hbx/hbx_constants.hpp>
|
||||
#include <uhdlib/usrp/dboard/hbx/hbx_dboard.hpp>
|
||||
#include <stdlib.h>
|
||||
#include <boost/test/unit_test.hpp>
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace uhd::usrp;
|
||||
|
||||
namespace uhd { namespace test {
|
||||
|
||||
namespace {
|
||||
constexpr double DEFAULT_MCR = 1250e6;
|
||||
}
|
||||
|
||||
//! Mock MPM server for X4xx/HBX
|
||||
//
|
||||
// This is a mock server that mimics an X4xx with an HBX daughterboard.
|
||||
class x4xx_hbx_mock_rpc_server : public x400_rpc_iface,
|
||||
public mpmd_rpc_iface,
|
||||
public dboard_base_rpc_iface,
|
||||
public hbx_rpc_iface,
|
||||
public dio_rpc_iface
|
||||
{
|
||||
public:
|
||||
x4xx_hbx_mock_rpc_server(const uhd::device_addr_t& device_info)
|
||||
: _device_info(device_info)
|
||||
{
|
||||
}
|
||||
|
||||
uhd::rpc_client::sptr get_raw_rpc_client() override
|
||||
{
|
||||
UHD_THROW_INVALID_CODE_PATH();
|
||||
}
|
||||
|
||||
/**************************************************************************
|
||||
* RPC Call Mockups
|
||||
*************************************************************************/
|
||||
size_t get_num_timekeepers() override
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::vector<std::string> get_mb_sensors() override
|
||||
{
|
||||
return {"ref_locked"};
|
||||
}
|
||||
|
||||
std::vector<std::string> get_gpio_banks() override
|
||||
{
|
||||
return {"GPIO0", "GPIO1"};
|
||||
}
|
||||
|
||||
bool supports_feature(const std::string& feature) override
|
||||
{
|
||||
return feature == "ref_clk_calibration";
|
||||
}
|
||||
|
||||
std::vector<std::map<std::string, std::string>> get_dboard_info() override
|
||||
{
|
||||
return {{{"pid", std::to_string(uhd::usrp::hbx::HBX_PID)}}};
|
||||
}
|
||||
|
||||
bool is_db_gpio_ifc_present(const size_t) override
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void set_tick_period(const size_t, const uint64_t) override
|
||||
{
|
||||
// nop
|
||||
}
|
||||
|
||||
double get_master_clock_rate() override
|
||||
{
|
||||
return _device_info.cast<double>("master_clock_rate", DEFAULT_MCR);
|
||||
}
|
||||
|
||||
std::vector<std::string> get_sensors(const std::string&) override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> get_sensor(
|
||||
const std::string&, const std::string&, size_t) override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
void set_cal_frozen(bool, size_t, size_t, size_t) override
|
||||
{
|
||||
// nop
|
||||
}
|
||||
|
||||
std::vector<int> get_cal_frozen(size_t, size_t, size_t) override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
void set_calibration_mode(size_t, size_t, size_t, std::string) override
|
||||
{
|
||||
// nop
|
||||
}
|
||||
|
||||
std::vector<std::vector<double>> get_cal_coefs(
|
||||
size_t, size_t, size_t, size_t) override
|
||||
{
|
||||
return {{0.0}};
|
||||
}
|
||||
|
||||
std::map<std::string, std::vector<uint8_t>> get_db_eeprom(const size_t) override
|
||||
{
|
||||
return {{{"pid", s2u8("mock")}, {"serial", s2u8("HBX_MOCK_NO_CAL")}}};
|
||||
}
|
||||
|
||||
double get_dboard_prc_rate() override
|
||||
{
|
||||
const double mcr = get_master_clock_rate();
|
||||
static const std::map<double, double> prc_rate_map{
|
||||
// One line per entry
|
||||
{1250e6, 62.5e6},
|
||||
{491.52e6, 61.44e6},
|
||||
{245.76e6, 61.44e6},
|
||||
{250e6, 62.5e6},
|
||||
// End of entries
|
||||
};
|
||||
const auto it = prc_rate_map.find(mcr);
|
||||
if (it != prc_rate_map.end()) {
|
||||
return it->second;
|
||||
}
|
||||
// Fallback to an LMX2572-supported reference frequency
|
||||
return 62.5e6;
|
||||
}
|
||||
|
||||
double rfdc_set_nco_freq(const std::string& trx,
|
||||
const size_t /*db_id*/,
|
||||
const size_t chan,
|
||||
const double freq,
|
||||
const size_t /*ch_mode*/) override
|
||||
{
|
||||
BOOST_REQUIRE(trx == "rx" || trx == "tx");
|
||||
BOOST_REQUIRE(chan < uhd::usrp::hbx::HBX_MAX_NUM_CHANS);
|
||||
nco_freq[trx][chan] = freq;
|
||||
return freq;
|
||||
}
|
||||
|
||||
double rfdc_get_nco_freq(const std::string& trx,
|
||||
const size_t /*db_id*/,
|
||||
const size_t chan,
|
||||
const size_t /*ch_mode*/) override
|
||||
{
|
||||
BOOST_REQUIRE(trx == "rx" || trx == "tx");
|
||||
BOOST_REQUIRE(chan < uhd::usrp::hbx::HBX_MAX_NUM_CHANS);
|
||||
if (nco_freq.find(trx) == nco_freq.end()
|
||||
|| nco_freq.at(trx).find(chan) == nco_freq.at(trx).end()) {
|
||||
return 0;
|
||||
}
|
||||
return nco_freq.at(trx).at(chan);
|
||||
}
|
||||
|
||||
double get_dboard_sample_rate() override
|
||||
{
|
||||
const double mcr = _device_info.cast<double>("master_clock_rate", DEFAULT_MCR);
|
||||
static const std::map<double, double> spll_map{
|
||||
// One line per entry
|
||||
{1250e6, 2.5e9},
|
||||
{491.52e6, 3932160000.0},
|
||||
{245.76e6, 1966080000.0},
|
||||
{250e6, 2e9},
|
||||
// End of entries
|
||||
};
|
||||
return spll_map.at(mcr);
|
||||
}
|
||||
|
||||
std::vector<std::map<std::string, std::string>> pop_host_tasks(
|
||||
const std::string&) override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
void enable_iq_swap(
|
||||
const bool is_band_inverted, const std::string& trx, const size_t chan) override
|
||||
{
|
||||
BOOST_REQUIRE(trx == "rx" || trx == "tx");
|
||||
BOOST_REQUIRE(chan < uhd::usrp::hbx::HBX_MAX_NUM_CHANS);
|
||||
iq_swap[trx][chan] = is_band_inverted;
|
||||
}
|
||||
|
||||
void set_data_path(const size_t mode, const std::string& trx) override
|
||||
{
|
||||
BOOST_REQUIRE(trx == "rx" || trx == "tx");
|
||||
data_path[trx] = mode;
|
||||
}
|
||||
|
||||
std::vector<std::string> get_gpio_srcs(const std::string& /*bank*/) override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
uint64_t get_timekeeper_time(size_t /*timekeeper_idx*/, bool /*last_pps*/) override
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
void set_timekeeper_time(
|
||||
size_t /*timekeeper_idx*/, uint64_t /*ticks*/, bool /*last_pps*/) override
|
||||
{
|
||||
// nop
|
||||
}
|
||||
|
||||
std::string get_time_source() override
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
std::vector<std::string> get_time_sources() override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string get_clock_source() override
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
std::vector<std::string> get_clock_sources() override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> get_sync_source() override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<std::map<std::string, std::string>> get_sync_sources() override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
void set_clock_source_out(bool /*enb*/) override
|
||||
{
|
||||
// nop
|
||||
}
|
||||
|
||||
void set_trigger_io(const std::string& /*direction*/) override
|
||||
{
|
||||
// nop
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> get_mb_eeprom() override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<std::string> get_gpio_src(const std::string& /*bank*/) override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
void set_gpio_src(
|
||||
const std::string& /*bank*/, const std::vector<std::string>& /*src*/) override
|
||||
{
|
||||
// nop
|
||||
}
|
||||
|
||||
void set_ref_clk_tuning_word(uint32_t /*tuning_word*/) override
|
||||
{
|
||||
// nop
|
||||
}
|
||||
|
||||
uint32_t get_ref_clk_tuning_word() override
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
void store_ref_clk_tuning_word(uint32_t /*tuning_word*/) override
|
||||
{
|
||||
// nop
|
||||
}
|
||||
|
||||
sensor_value_t::sensor_map_t get_mb_sensor(const std::string& /*sensor*/) override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
void set_time_source(const std::string& /*source*/) override
|
||||
{
|
||||
// nop
|
||||
}
|
||||
|
||||
void set_clock_source(const std::string& /*source*/) override
|
||||
{
|
||||
// nop
|
||||
}
|
||||
|
||||
void set_sync_source(const std::map<std::string, std::string>& /*source*/) override
|
||||
{
|
||||
// nop
|
||||
}
|
||||
|
||||
bool get_threshold_status(size_t /*db_number*/,
|
||||
size_t /*chan*/,
|
||||
size_t /*mode*/,
|
||||
size_t /*threshold_block*/) override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
void set_dac_mux_enable(
|
||||
size_t /*db_number*/, size_t /*chan*/, int /*enable*/, size_t /*mode*/) override
|
||||
{
|
||||
// nop
|
||||
}
|
||||
|
||||
void set_dac_mux_data(size_t /*i*/, size_t /*q*/) override
|
||||
{
|
||||
// nop
|
||||
}
|
||||
|
||||
double get_spll_freq() override
|
||||
{
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
void setup_threshold(size_t /*db_number*/,
|
||||
size_t /*chan*/,
|
||||
size_t /*mix_mode*/,
|
||||
size_t /*threshold_block*/,
|
||||
const std::string& /*mode*/,
|
||||
size_t /*delay*/,
|
||||
size_t /*under*/,
|
||||
size_t /*over*/) override
|
||||
{
|
||||
// nop
|
||||
}
|
||||
|
||||
void restart_converter(const std::string& /*trx*/,
|
||||
size_t /*block_count*/,
|
||||
size_t /*chan*/,
|
||||
size_t /*mixer_mode*/)
|
||||
{
|
||||
// nop
|
||||
}
|
||||
|
||||
std::vector<std::string> dio_get_supported_voltage_levels(const std::string&) override
|
||||
{
|
||||
return {"OFF", "1V8", "2V5", "3V3"};
|
||||
}
|
||||
|
||||
void dio_set_voltage_level(const std::string&, const std::string&) override
|
||||
{
|
||||
// nop
|
||||
}
|
||||
|
||||
std::string dio_get_voltage_level(const std::string&) override
|
||||
{
|
||||
return "3V3";
|
||||
}
|
||||
|
||||
void dio_set_port_mapping(const std::string&) override
|
||||
{
|
||||
// nop
|
||||
}
|
||||
|
||||
void dio_set_pin_directions(const std::string&, uint32_t) override
|
||||
{
|
||||
// nop
|
||||
}
|
||||
|
||||
void dio_set_external_power(const std::string&, bool) override
|
||||
{
|
||||
// nop
|
||||
}
|
||||
|
||||
std::string dio_get_external_power_state(const std::string&) override
|
||||
{
|
||||
return "OFF";
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> synchronize(
|
||||
const std::map<std::string, std::string>&, bool) override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> aggregate_sync_data(
|
||||
const std::list<std::map<std::string, std::string>>& collated_sync_data) override
|
||||
{
|
||||
return collated_sync_data.front();
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Public attributes for easy inspection
|
||||
std::map<std::string, std::map<size_t, double>> nco_freq;
|
||||
std::map<std::string, std::map<size_t, bool>> iq_swap;
|
||||
std::map<std::string, size_t> data_path;
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private:
|
||||
uhd::device_addr_t _device_info;
|
||||
|
||||
static std::vector<uint8_t> s2u8(const std::string& s)
|
||||
{
|
||||
return std::vector<uint8_t>(s.begin(), s.end());
|
||||
}
|
||||
};
|
||||
|
||||
}} // namespace uhd::test
|
||||
Loading…
Reference in New Issue
Block a user