diff --git a/lib/src/phy/rf/CMakeLists.txt b/lib/src/phy/rf/CMakeLists.txt index cd39c8e21..fe68e8c20 100644 --- a/lib/src/phy/rf/CMakeLists.txt +++ b/lib/src/phy/rf/CMakeLists.txt @@ -43,6 +43,12 @@ if(RF_FOUND) endif(UHD_ENABLE_CUSTOM_RFNOC) endif (UHD_FOUND) + if (UHD_FOUND AND UHD_ENABLE_CUSTOM_RFNOC) + add_executable(rfnoc_test rfnoc_test.cc) + target_link_libraries(rfnoc_test srslte_rf ${UHD_LIBRARIES} ${Boost_LIBRARIES} /usr/lib/x86_64-linux-gnu/libboost_system.so) + message(info ${Boost_LIBRARIES}) + endif (UHD_FOUND AND UHD_ENABLE_CUSTOM_RFNOC) + if (BLADERF_FOUND) add_definitions(-DENABLE_BLADERF) list(APPEND SOURCES_RF rf_blade_imp.c) diff --git a/lib/src/phy/rf/rf_uhd_rfnoc.h b/lib/src/phy/rf/rf_uhd_rfnoc.h index d98e83169..1dd552245 100644 --- a/lib/src/phy/rf/rf_uhd_rfnoc.h +++ b/lib/src/phy/rf/rf_uhd_rfnoc.h @@ -72,12 +72,15 @@ private: const size_t spp = 1920 / 8; // Primary parameters + bool loopback = false; double master_clock_rate = 184.32e6; double bw_hz = 100.0; size_t nof_radios = 1; ///< Number of RF devices in the device size_t nof_channels = 1; ///< Number of Channels per Radio std::vector rx_freq_hz; + std::vector rx_center_freq_hz; std::vector tx_freq_hz; + std::vector tx_center_freq_hz; size_t dma_fifo_depth = 8192UL * 4096UL; // Radio control @@ -126,6 +129,13 @@ private: // Parse master clock rate parse_param(args, "master_clock_rate", master_clock_rate, false); + // Parser loopback (optional) + loopback = args.has_key("loopback"); + if (loopback) { + args.pop("loopback"); + Warning("RFNOC Loopback activated!") + } + // Parse number of radios parse_param(args, "rfnoc_nof_radios", nof_radios); if (nof_radios == 0) { @@ -143,7 +153,9 @@ private: // Set secondary parameters bw_hz = master_clock_rate / 2.0; tx_freq_hz.resize(nof_radios * nof_channels); + tx_center_freq_hz.resize(nof_radios); rx_freq_hz.resize(nof_radios * nof_channels); + rx_center_freq_hz.resize(nof_radios); return UHD_ERROR_NONE; } @@ -153,9 +165,12 @@ private: UHD_SAFE_C_SAVE_ERROR( this, // Create Radio control - radio_ctrl.resize(nof_radios); - radio_id.resize(nof_radios); - for (size_t i = 0; i < nof_radios; i++) { + if (not loopback) { + radio_ctrl.resize(nof_radios); + radio_id.resize(nof_radios); + } + + for (size_t i = 0; i < radio_ctrl.size(); i++) { // Create handle for radio object radio_id[i] = uhd::rfnoc::block_id_t(0, RADIO_BLOCK_NAME, i); // This next line will fail if the radio is not actually available @@ -210,6 +225,7 @@ private: if (ddc_ctrl[i] == nullptr) { ddc_id[i] = uhd::rfnoc::block_id_t(0, DDC_BLOCK_NAME, i); ddc_ctrl[i] = device3->get_block_ctrl(ddc_id[i]); + ddc_ctrl[i]->clear(); } for (size_t j = 0; j < nof_channels; j++) { @@ -244,6 +260,7 @@ private: if (duc_ctrl[i] == nullptr) { duc_id[i] = uhd::rfnoc::block_id_t(0, DUC_BLOCK_NAME, i); duc_ctrl[i] = device3->get_block_ctrl(duc_id[i]); + duc_ctrl[i]->clear(); } for (size_t j = 0; j < nof_channels; j++) { @@ -307,6 +324,35 @@ private: }) } + uhd_error connect_loopback() + { + size_t nof_samples_per_packet = 0; + + if (spp != 0) { + nof_samples_per_packet = spp * 4 + 2 * sizeof(uint64_t); + } + + UHD_SAFE_C_SAVE_ERROR(this, + + // Get Tx and Rx Graph + graph = device3->create_graph("graph"); + + for (size_t i = 0; i < nof_radios; i++) { + // DUC -> DDC + UHD_LOG_DEBUG(graph->get_name(), "Connecting " << duc_id[i] << " -> " << ddc_id[i]); + graph->connect(duc_id[i], 0, ddc_id[i], 0, nof_samples_per_packet); + + for (size_t j = 0; j < nof_channels; j++) { + // DMA FIFO -> DUC + size_t dma_port = i * nof_channels + j; + UHD_LOG_DEBUG(graph->get_name(), + "Connecting " << dma_id << ":" << dma_port << " -> " << duc_id[i] << ":" + << j); + graph->connect(dma_id, dma_port, duc_id[i], j, nof_samples_per_packet); + } + }) + } + public: static bool is_required(const uhd::device_addr_t& device_addr) { @@ -360,7 +406,11 @@ public: } // Connect components - err = connect(); + if (loopback) { + err = connect_loopback(); + } else { + err = connect(); + } if (err != UHD_ERROR_NONE) { return err; } @@ -402,10 +452,19 @@ public: } uhd_error get_time_now(uhd::time_spec_t& timespec) override { + if (loopback) { + timespec = 0.0; + return UHD_ERROR_NONE; + } + UHD_SAFE_C_SAVE_ERROR(this, timespec = radio_ctrl[0]->get_time_now();); } uhd_error set_sync_source(const std::string& source) override { + if (loopback) { + return UHD_ERROR_NONE; + } + UHD_SAFE_C_SAVE_ERROR(this, for (size_t radio_idx = 0; radio_idx < nof_radios; radio_idx++) { UHD_LOG_DEBUG(radio_id[radio_idx], "Setting sync source to " << source); radio_ctrl[radio_idx]->set_clock_source(source); @@ -495,6 +554,10 @@ public: return UHD_ERROR_INDEX; } + if (radio_ctrl.size() == 0) { + return UHD_ERROR_NONE; + } + size_t radio_idx = ch / nof_channels; size_t channel_idx = ch % nof_channels; @@ -517,6 +580,10 @@ public: return UHD_ERROR_INDEX; } + if (radio_ctrl.size() == 0) { + return UHD_ERROR_NONE; + } + size_t radio_idx = ch / nof_channels; size_t channel_idx = ch % nof_channels; @@ -531,8 +598,23 @@ public: radio_ctrl[radio_idx]->set_rx_gain(gain, 0); UHD_LOG_DEBUG(radio_id[radio_idx], "Actual RX Gain: " << radio_ctrl[radio_idx]->get_rx_gain(0) << " dB...");) } - uhd_error get_tx_gain(double& gain) override { UHD_SAFE_C_SAVE_ERROR(this, gain = radio_ctrl[0]->get_tx_gain(0);) } - uhd_error get_rx_gain(double& gain) override { UHD_SAFE_C_SAVE_ERROR(this, gain = radio_ctrl[0]->get_rx_gain(0);) } + uhd_error get_tx_gain(double& gain) override + { + if (radio_ctrl.size() == 0) { + return UHD_ERROR_NONE; + } + + UHD_SAFE_C_SAVE_ERROR(this, gain = radio_ctrl[0]->get_tx_gain(0);) + } + uhd_error get_rx_gain(double& gain) override + { + + if (radio_ctrl.size() == 0) { + return UHD_ERROR_NONE; + } + + UHD_SAFE_C_SAVE_ERROR(this, gain = radio_ctrl[0]->get_rx_gain(0);) + } uhd_error set_tx_freq(uint32_t ch, double target_freq, double& actual_freq) override { if (ch >= tx_freq_hz.size()) { @@ -557,31 +639,30 @@ public: Debug("Tuning Tx " << ch << " to " << actual_freq / 1e6 << " MHz..."); size_t i = ch / nof_channels; - - // Calculate center frequency from average - double center_freq_hz = 0.0; - for (size_t j = 0; j < nof_channels; j++) { - if (not std::isnormal(tx_freq_hz[i * nof_channels + j])) { - tx_freq_hz[i * nof_channels + j] = target_freq; - } - center_freq_hz += tx_freq_hz[i * nof_channels + j]; - } - center_freq_hz /= nof_channels; + size_t j = ch % nof_channels; UHD_SAFE_C_SAVE_ERROR( this, // Set Radio Tx freq - UHD_LOG_DEBUG(radio_id[i], "Setting TX Freq: " << center_freq_hz / 1e6 << " MHz..."); - radio_ctrl[i]->set_tx_frequency(center_freq_hz, 0); - center_freq_hz = radio_ctrl[i]->get_tx_frequency(0); - UHD_LOG_DEBUG(radio_id[i], "Actual TX Freq: " << center_freq_hz / 1e6 << " MHz..."); + if (not std::isnormal(tx_center_freq_hz[i]) and not loopback) { + UHD_LOG_DEBUG(radio_id[i], + "Setting TX Freq: " << target_freq / 1e6 << " (" << uint64_t(target_freq) << ") MHz..."); + radio_ctrl[i]->set_tx_frequency(target_freq, 0); + tx_center_freq_hz[i] = radio_ctrl[i]->get_tx_frequency(0); + UHD_LOG_DEBUG(radio_id[i], + "Actual TX Freq: " << tx_center_freq_hz[i] / 1e6 << "(" << uint64_t(tx_center_freq_hz[i]) + << ") MHz..."); + } // Setup DUC - for (size_t j = 0; j < nof_channels; j++) { - double freq_hz = tx_freq_hz[nof_channels * i + j] - center_freq_hz; - UHD_LOG_DEBUG(duc_id[i], "Setting " << j << " freq: " << freq_hz / 1e6 << " MHz ..."); + double freq_hz = tx_freq_hz[nof_channels * i + j] - tx_center_freq_hz[i]; + if (std::round(duc_ctrl[i]->get_arg("freq") != freq_hz)) { + UHD_LOG_DEBUG(duc_id[i], + "Setting " << j << " freq: " << freq_hz / 1e6 << "(" << uint64_t(freq_hz) << ") MHz ..."); duc_ctrl[i]->set_arg("freq", std::to_string(freq_hz), j); + freq_hz = duc_ctrl[i]->get_arg("freq"); + Debug("Actual Tx " << ch << " freq error: " << tx_center_freq_hz[i] + freq_hz - target_freq << " Hz..."); }) } uhd_error set_rx_freq(uint32_t ch, double target_freq, double& actual_freq) override @@ -608,31 +689,26 @@ public: Debug("Tuning Rx " << ch << " to " << actual_freq / 1e6 << " MHz..."); size_t i = ch / nof_channels; - - // Calculate center frequency from average - double center_freq_hz = 0.0; - for (size_t j = 0; j < nof_channels; j++) { - if (not std::isnormal(rx_freq_hz[i * nof_channels + j])) { - rx_freq_hz[i * nof_channels + j] = target_freq; - } - center_freq_hz += rx_freq_hz[i * nof_channels + j]; - } - center_freq_hz /= nof_channels; + size_t j = ch % nof_channels; UHD_SAFE_C_SAVE_ERROR( this, // Set Radio Tx freq - UHD_LOG_DEBUG(radio_id[i], "Setting RX Freq: " << center_freq_hz / 1e6 << " MHz..."); - radio_ctrl[i]->set_rx_frequency(center_freq_hz, 0); - center_freq_hz = radio_ctrl[i]->get_rx_frequency(0); - UHD_LOG_DEBUG(radio_id[i], "Actual RX Freq: " << center_freq_hz / 1e6 << " MHz..."); + if (not std::isnormal(rx_center_freq_hz[i]) and not loopback) { + UHD_LOG_DEBUG(radio_id[i], "Setting RX Freq: " << target_freq / 1e6 << " MHz..."); + radio_ctrl[i]->set_rx_frequency(target_freq, 0); + rx_center_freq_hz[i] = radio_ctrl[i]->get_rx_frequency(0); + UHD_LOG_DEBUG(radio_id[i], "Actual RX Freq: " << rx_center_freq_hz[i] / 1e6 << " MHz..."); + } // Setup DDC - for (size_t j = 0; j < nof_channels; j++) { - double freq_hz = center_freq_hz - rx_freq_hz[nof_channels * i + j]; + double freq_hz = rx_center_freq_hz[i] - rx_freq_hz[nof_channels * i + j]; + if (std::round(ddc_ctrl[i]->get_arg("freq") != freq_hz)) { UHD_LOG_DEBUG(ddc_id[i], "Setting " << j << " freq: " << freq_hz / 1e6 << " MHz ..."); ddc_ctrl[i]->set_arg("freq", freq_hz, j); + freq_hz = ddc_ctrl[i]->get_arg("freq"); + Debug("Actual Rx " << ch << " freq error: " << rx_center_freq_hz[i] - freq_hz - target_freq << " Hz..."); }) } }; diff --git a/lib/src/phy/rf/rf_uhd_safe.h b/lib/src/phy/rf/rf_uhd_safe.h index 46f503dfc..3bd2b11ac 100644 --- a/lib/src/phy/rf/rf_uhd_safe.h +++ b/lib/src/phy/rf/rf_uhd_safe.h @@ -129,8 +129,8 @@ public: UHD_SAFE_C_SAVE_ERROR(this, uhd::stream_cmd_t stream_cmd(uhd::stream_cmd_t::STREAM_MODE_START_CONTINUOUS); stream_cmd.time_spec = time_spec; - stream_cmd.time_spec += 0.1; - stream_cmd.stream_now = false; + stream_cmd.time_spec += delay; + stream_cmd.stream_now = not std::isnormal(delay); rx_stream->issue_stream_cmd(stream_cmd);) } diff --git a/lib/src/phy/rf/rfnoc_test.cc b/lib/src/phy/rf/rfnoc_test.cc new file mode 100644 index 000000000..3bceb9344 --- /dev/null +++ b/lib/src/phy/rf/rfnoc_test.cc @@ -0,0 +1,357 @@ +// +// Copyright 2016 Ettus Research LLC +// Copyright 2018 Ettus Research, a National Instruments Company +// +// SPDX-License-Identifier: GPL-3.0-or-later +// + +// Example UHD/RFNoC application: Connect an rx radio to a tx radio and +// run a loopback. +#include "rf_uhd_rfnoc.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define TESTASSERT(cond) \ + do { \ + if (!(cond)) { \ + printf("[%s][Line %d] Fail at \"%s\"\n", __FUNCTION__, __LINE__, (#cond)); \ + return -1; \ + } \ + } while (0) + +/**************************************************************************** + * main + ***************************************************************************/ +cf_t cexp_(float arg) +{ + std::complex v = std::polar(0.5f, arg); + return *((cf_t*)&v); +} + +struct rfnoc_custom_testbench_s { +public: + typedef struct { + // Main parameters + std::string ip_address = "192.168.40.2"; + double duration_s = 1.0; + double master_clock_rate_hz = 184.32e6; + double srate_hz = 1.92e6; + double init_tx_time = 0.1; + size_t period_nsamples = 19; + uint32_t nof_channels = 2; + uint32_t nof_radios = 1; + bool asynchronous_msg = false; + bool blocking = false; + size_t blocking_min_tx = 1; + + // Getters + float get_freq_norm() { return 1.0f / float(period_nsamples); } + } config_s; + +private: + config_s config; + + std::mutex mutex; + std::condition_variable tx_cvar; + std::condition_variable rx_cvar; + + cf_t* tx_buffer[2] = {}; + cf_t* rx_buffer[2] = {}; + + std::shared_ptr rfnoc; + size_t packet_size_nsamples = 0; + + // Stats + size_t txd_nsamples_total = 0; + size_t txd_count = 0; + size_t rxd_nsamples_total = 0; + size_t rxd_count = 0; + bool running = true; + + void check_async_msg() + { + bool valid = false; + uhd::async_metadata_t async_metadata; + + rfnoc->recv_async_msg(async_metadata, 0.0, valid); + + if (valid) { + switch (async_metadata.event_code) { + + case uhd::async_metadata_t::EVENT_CODE_BURST_ACK: + Warning("BURST ACK") break; + case uhd::async_metadata_t::EVENT_CODE_UNDERFLOW: + Warning("UNDERFLOW") break; + case uhd::async_metadata_t::EVENT_CODE_SEQ_ERROR: + Warning("SEQUENCE ERROR") break; + case uhd::async_metadata_t::EVENT_CODE_TIME_ERROR: + Warning("TIME ERROR") break; + case uhd::async_metadata_t::EVENT_CODE_UNDERFLOW_IN_PACKET: + Warning("UNDERFLOW IN PACKET") break; + case uhd::async_metadata_t::EVENT_CODE_SEQ_ERROR_IN_BURST: + Warning("ERROR IN BURST") break; + case uhd::async_metadata_t::EVENT_CODE_USER_PAYLOAD: + Warning("ERROR USER PAYLOAD") break; + } + } + } + + void* tx_routine() + { + size_t txd_nsamples = 0; + void* tx_buffs_ptr[] = {tx_buffer[0], tx_buffer[1]}; + + uhd::tx_metadata_t tx_metadata; + tx_metadata.has_time_spec = true; + tx_metadata.time_spec = config.init_tx_time; + tx_metadata.start_of_burst = true; + + while (tx_metadata.time_spec < config.duration_s + config.init_tx_time and running) { + cf_t v = cexp_(2.0f * M_PI * float(txd_nsamples_total % config.period_nsamples) * config.get_freq_norm()); + srslte_vec_gen_sine(v, config.get_freq_norm(), tx_buffer[0], packet_size_nsamples); + srslte_vec_sc_prod_cfc(tx_buffer[0], -1, tx_buffer[1], packet_size_nsamples); + + if (config.blocking) { + std::unique_lock lock(mutex); + while (rxd_count + config.blocking_min_tx <= txd_count and running) { + std::cv_status status = rx_cvar.wait_for(lock, std::chrono::milliseconds(100)); + if (status == std::cv_status::timeout) { + running = false; + } + } + } + + // Actual transmission + rfnoc->send(tx_buffs_ptr, packet_size_nsamples, tx_metadata, 0.1, txd_nsamples); + tx_metadata.start_of_burst = false; + + // Update Tx state + mutex.lock(); + txd_nsamples_total += txd_nsamples; + txd_count++; + tx_cvar.notify_all(); + mutex.unlock(); + tx_metadata.time_spec += packet_size_nsamples / config.srate_hz; + tx_metadata.start_of_burst = false; + + if (not config.asynchronous_msg) { + check_async_msg(); + } + } + + tx_metadata.end_of_burst = true; + rfnoc->send(tx_buffs_ptr, 0, tx_metadata, 0.1, txd_nsamples); + + return nullptr; + } + + void* rx_routine() + { + void* rx_buffs_ptr[] = {rx_buffer[0], rx_buffer[1]}; + + do { + uhd::rx_metadata_t rx_metadata; + size_t rxd_nsamples = 0; + + if (config.blocking) { + std::unique_lock lock(mutex); + while (rxd_count >= txd_count and running) { + std::cv_status status = tx_cvar.wait_for(lock, std::chrono::milliseconds(100)); + if (status == std::cv_status::timeout) { + running = false; + } + } + } + + rfnoc->receive(rx_buffs_ptr, packet_size_nsamples, rx_metadata, 0.1, false, rxd_nsamples); + + std::unique_lock lock(mutex); + if (rx_metadata.error_code == uhd::rx_metadata_t::ERROR_CODE_TIMEOUT) { + running = false; + } else { + // Update stats + rxd_nsamples_total += rxd_nsamples; + rxd_count++; + float avg_power = srslte_vec_avg_power_cf(rx_buffer[0], rxd_nsamples); + if (std::isnan(avg_power) or avg_power > 1e-6) { + Warning("-- RX " << rxd_count << " Received " << avg_power); + } + } + rx_cvar.notify_all(); + } while (running); + + return nullptr; + } + + void* async_msg_routine() + { + while (running) { + check_async_msg(); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + return nullptr; + } + +public: + rfnoc_custom_testbench_s(const config_s& config_) + { + rfnoc = std::make_shared(); + + config = config_; + } + int init() + { + uhd::device_addr_t hint; + hint["ip_addr"] = config.ip_address; + hint["master_clock_rate"] = std::to_string(config.master_clock_rate_hz); + hint["rfnoc_nof_channels"] = std::to_string(config.nof_channels); + hint["rfnoc_nof_radios"] = std::to_string(config.nof_radios); + hint["loopback"] = "true"; + uhd::log::set_console_level(uhd::log::severity_level::trace); + + if (rfnoc->usrp_make(hint, config.nof_channels * config.nof_radios) != UHD_ERROR_NONE) { + Warning(rfnoc->last_error); + return SRSLTE_ERROR; + } + + if (rfnoc->set_tx_rate(config.srate_hz) != UHD_ERROR_NONE) { + Warning(rfnoc->last_error); + return SRSLTE_ERROR; + } + if (rfnoc->set_rx_rate(config.srate_hz) != UHD_ERROR_NONE) { + Warning(rfnoc->last_error); + return SRSLTE_ERROR; + } + + size_t nof_tx_samples = 0; + size_t nof_rx_samples = 0; + rfnoc->get_tx_stream(nof_tx_samples); + rfnoc->get_rx_stream(nof_rx_samples); + packet_size_nsamples = SRSLTE_MIN(nof_tx_samples, nof_rx_samples); + + for (size_t j = 0; j < config.nof_channels; j++) { + tx_buffer[j] = srslte_vec_cf_malloc(packet_size_nsamples); + rx_buffer[j] = srslte_vec_cf_malloc(packet_size_nsamples); + } + + if (rfnoc->start_rx_stream(config.init_tx_time) != UHD_ERROR_NONE) { + Warning(rfnoc->last_error); + return SRSLTE_ERROR; + } + + return SRSLTE_SUCCESS; + } + + int run() + { + running = true; + + std::thread tx_thread(&rfnoc_custom_testbench_s::tx_routine, this); + std::thread rx_thread(&rfnoc_custom_testbench_s::rx_routine, this); + + if (config.asynchronous_msg) { + std::thread async_msg_thread(&rfnoc_custom_testbench_s::async_msg_routine, this); + async_msg_thread.join(); + } + + tx_thread.join(); + rx_thread.join(); + + Info("Tx packets: " << txd_count); + Info("Tx samples: " << txd_nsamples_total); + + Info("Rx packets: " << rxd_count); + Info("Rx samples: " << rxd_nsamples_total); + + return SRSLTE_SUCCESS; + } + + ~rfnoc_custom_testbench_s() + { + for (size_t j = 0; j < config.nof_channels; j++) { + if (tx_buffer[j] != nullptr) { + free(tx_buffer[j]); + } + if (rx_buffer[j] != nullptr) { + free(rx_buffer[j]); + } + } + } +}; + +static int test(const rfnoc_custom_testbench_s::config_s& config) +{ + rfnoc_custom_testbench_s context(config); + + TESTASSERT(context.init() == SRSLTE_SUCCESS); + TESTASSERT(context.run() == SRSLTE_SUCCESS); + + return SRSLTE_SUCCESS; +} + +namespace bpo = boost::program_options; + +static int parse_args(int argc, char** argv, rfnoc_custom_testbench_s::config_s& args) +{ + int ret = SRSLTE_SUCCESS; + + bpo::options_description options; + bpo::options_description common("Common execution options"); + + // clang-format off + common.add_options() + ("ip_addr", bpo::value(&args.ip_address), "Device 3 IP address") + ("duration", bpo::value(&args.duration_s), "Duration of the execution in seconds") + ("master_clock_rate", bpo::value(&args.master_clock_rate_hz), "Device master clock rate in Hz") + ("srate", bpo::value(&args.srate_hz), "Sampling rate in Hz") + ("init_time", bpo::value(&args.init_tx_time), "Initial time in seconds") + ("period_nsamples", bpo::value(&args.period_nsamples), "Sine wave period in number of samples") + ("asynch_msg", bpo::value(&args.asynchronous_msg), "Enable/disable asynchronous message") + ("blocking", bpo::value(&args.blocking), "Block transmitter/receiver for every transaction") + ("blocking_min_tx", bpo::value(&args.blocking_min_tx),"Ensure a minimum of enqueued Tx") + ; + options.add(common).add_options()("help", "Show this message"); + // clang-format on + + bpo::variables_map vm; + try { + bpo::store(bpo::command_line_parser(argc, argv).options(options).run(), vm); + bpo::notify(vm); + } catch (bpo::error& e) { + std::cerr << e.what() << std::endl; + ret = SRSLTE_ERROR; + } + + // help option was given or error - print usage and exit + if (vm.count("help") || ret) { + std::cout << "Usage: " << argv[0] << " [OPTIONS] config_file" << std::endl << std::endl; + std::cout << options << std::endl << std::endl; + ret = SRSLTE_ERROR; + } + + return ret; +} + +int main(int argc, char* argv[]) +{ + rfnoc_custom_testbench_s::config_s config; + + TESTASSERT(parse_args(argc, argv, config) == SRSLTE_SUCCESS); + + TESTASSERT(test(config) == SRSLTE_SUCCESS); + + return EXIT_SUCCESS; +}