RFNOC: added DUC/DDC internal loopback option and test

This commit is contained in:
Xavier Arteaga 2020-07-15 13:06:25 +02:00 committed by Xavier Arteaga
parent dcf05f7a53
commit f80c779d88
4 changed files with 480 additions and 41 deletions

View File

@ -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)

View File

@ -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<double> rx_freq_hz;
std::vector<double> rx_center_freq_hz;
std::vector<double> tx_freq_hz;
std::vector<double> 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<uhd::rfnoc::ddc_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<uhd::rfnoc::duc_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<double>("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<double>("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<double>("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<double>("freq");
Debug("Actual Rx " << ch << " freq error: " << rx_center_freq_hz[i] - freq_hz - target_freq << " Hz...");
})
}
};

View File

@ -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);)
}

View File

@ -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 <boost/program_options.hpp>
#include <boost/program_options/cmdline.hpp>
#include <boost/program_options/options_description.hpp>
#include <boost/program_options/variables_map.hpp>
#include <chrono>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <srslte/config.h>
#include <srslte/phy/utils/vector.h>
#include <uhd/device3.hpp>
#include <uhd/rfnoc/dma_fifo_block_ctrl.hpp>
#include <uhd/rfnoc/duc_ch2_block_ctrl.hpp>
#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<float> 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<rf_uhd_safe_interface> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<rf_uhd_rfnoc>();
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<std::string>(&args.ip_address), "Device 3 IP address")
("duration", bpo::value<double>(&args.duration_s), "Duration of the execution in seconds")
("master_clock_rate", bpo::value<double>(&args.master_clock_rate_hz), "Device master clock rate in Hz")
("srate", bpo::value<double>(&args.srate_hz), "Sampling rate in Hz")
("init_time", bpo::value<double>(&args.init_tx_time), "Initial time in seconds")
("period_nsamples", bpo::value<size_t>(&args.period_nsamples), "Sine wave period in number of samples")
("asynch_msg", bpo::value<bool>(&args.asynchronous_msg), "Enable/disable asynchronous message")
("blocking", bpo::value<bool>(&args.blocking), "Block transmitter/receiver for every transaction")
("blocking_min_tx", bpo::value<size_t>(&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;
}