From fb7623f5b636944818402c9ee56d7fc6cd0fa527 Mon Sep 17 00:00:00 2001 From: Xavier Arteaga Date: Thu, 13 May 2021 17:44:15 +0200 Subject: [PATCH] Initial intra frequency NR cell search and test --- srsue/hdr/phy/scell/intra_measure_base.h | 20 +- srsue/hdr/phy/scell/intra_measure_lte.h | 2 + srsue/hdr/phy/scell/intra_measure_nr.h | 117 ++++++ srsue/src/phy/scell/intra_measure_base.cc | 38 +- srsue/src/phy/scell/intra_measure_lte.cc | 33 +- srsue/src/phy/scell/intra_measure_nr.cc | 122 ++++++ srsue/test/phy/CMakeLists.txt | 10 + srsue/test/phy/nr_cell_search_test.cc | 453 ++++++++++++++++++++++ 8 files changed, 748 insertions(+), 47 deletions(-) create mode 100644 srsue/hdr/phy/scell/intra_measure_nr.h create mode 100644 srsue/src/phy/scell/intra_measure_nr.cc create mode 100644 srsue/test/phy/nr_cell_search_test.cc diff --git a/srsue/hdr/phy/scell/intra_measure_base.h b/srsue/hdr/phy/scell/intra_measure_base.h index c61a04892..db1343e4d 100644 --- a/srsue/hdr/phy/scell/intra_measure_base.h +++ b/srsue/hdr/phy/scell/intra_measure_base.h @@ -12,12 +12,13 @@ #ifndef SRSUE_INTRA_MEASURE_BASE_H #define SRSUE_INTRA_MEASURE_BASE_H +#include "srsran/interfaces/ue_phy_interfaces.h" +#include +#include #include #include #include -#include - -#include "scell_recv.h" +#include namespace srsue { namespace scell { @@ -161,15 +162,16 @@ private: { public: typedef enum { - idle = 0, ///< Initial state, internal thread runs, it does not capture data - wait, ///< Wait for the period time to pass - receive, ///< Accumulate samples in ring buffer - measure, ///< Module is busy measuring - quit ///< Quit thread, no transitions are allowed + initial = 0, /// Initial state, it transitions to idle once the internal thread has started + idle, ///< Internal thread runs, it does not capture data + wait, ///< Wait for the period time to pass + receive, ///< Accumulate samples in ring buffer + measure, ///< Module is busy measuring + quit ///< Quit thread, no transitions are allowed } state_t; private: - state_t state = idle; + state_t state = initial; std::mutex mutex; std::condition_variable cvar; diff --git a/srsue/hdr/phy/scell/intra_measure_lte.h b/srsue/hdr/phy/scell/intra_measure_lte.h index 2a9db1b71..edd7889d1 100644 --- a/srsue/hdr/phy/scell/intra_measure_lte.h +++ b/srsue/hdr/phy/scell/intra_measure_lte.h @@ -13,6 +13,8 @@ #define SRSRAN_INTRA_MEASURE_LTE_H #include "intra_measure_base.h" +#include "scell_recv.h" +#include namespace srsue { namespace scell { diff --git a/srsue/hdr/phy/scell/intra_measure_nr.h b/srsue/hdr/phy/scell/intra_measure_nr.h new file mode 100644 index 000000000..97ad2aae3 --- /dev/null +++ b/srsue/hdr/phy/scell/intra_measure_nr.h @@ -0,0 +1,117 @@ +/** + * + * \section COPYRIGHT + * + * Copyright 2013-2021 Software Radio Systems Limited + * + * By using this file, you agree to the terms and conditions set + * forth in the LICENSE file which can be found at the top level of + * the distribution. + * + */ +#ifndef SRSRAN_INTRA_MEASURE_NR_H +#define SRSRAN_INTRA_MEASURE_NR_H + +#include "intra_measure_base.h" +#include + +namespace srsue { +namespace scell { + +/** + * @brief Describes a class for performing LTE intra-frequency cell search and measurement + */ +class intra_measure_nr : public intra_measure_base +{ +public: + /** + * @brief Describes initialization arguments. It is used to preallocate all memory and avoiding performing memory + * allocation when the configuration is set + */ + struct args_t { + float rx_gain_offset_dB = 0.0f; + uint32_t max_len_ms = 1; + double max_srate_hz = 61.44e6; + srsran_subcarrier_spacing_t min_scs = srsran_subcarrier_spacing_15kHz; + float thr_snr_db = 5.0f; ///< minimum SNR threshold + }; + + /** + * @brief Describes the required configuration arguments to start measurements + */ + struct config_t { + uint32_t arfcn; ///< Carrier frequency in ARFCN + double srate_hz = 0.0; ///< Sampling rate in Hz, set to 0.0 for maximum + uint32_t len_ms = 1; ///< Amount of time to accumulate + uint32_t periodicity_ms = 20; ///< Accumulation trigger period + float rx_gain_offset_db = 0.0f; ///< Gain offset, for calibrated measurements + double center_freq_hz = 0.0; ///< Base-band center frequency in Hz + double ssb_freq_hz = 0.0; ///< SSB center frequency + srsran_subcarrier_spacing_t scs = srsran_subcarrier_spacing_30kHz; ///< SSB configured Subcarrier spacing + int serving_cell_pci = -1; ///< Current serving cell PCI, set to -1 if no + ///< serving cell has been configured for this + ///< carrier + }; + + /** + * @brief Constructor + * @param logger Logging object + * @param new_meas_itf_ Interface to report measurement to higher layers + */ + intra_measure_nr(srslog::basic_logger& logger, meas_itf& new_meas_itf_); + + /** + * @brief Destructor + */ + ~intra_measure_nr() override; + + /** + * @brief Initialises LTE specific measurement objects + * @param args Configuration arguments + * @return True if initialization is successful, false otherwise + */ + bool init(uint32_t cc_idx, const args_t& args); + + /** + * @brief Sets the primary cell and selects NR operation mode, configures the cell bandwidth and sampling rate + * @param arfcn Frequency the component is receiving base-band from. Used only for reporting the ARFCN to the RRC + * @param cfg Actual configuration + * @return True if configuration is successful, false otherwise + */ + bool set_config(uint32_t arfcn, const config_t& cfg); + + /** + * @brief Get current frequency number + * @return the current ARFCN + */ + uint32_t get_earfcn() const override { return current_arfcn; }; + +private: + /** + * @brief Provides with the RAT to the base class + * @return The RAT measured by this class which is NR + */ + srsran::srsran_rat_t get_rat() const override { return srsran::srsran_rat_t::nr; } + + /** + * @brief NR specific measurement process + * @attention It searches and measures the SSB with best SNR + * @param context Measurement context + * @param buffer Provides the baseband buffer to perform the measurements + */ + void measure_rat(const measure_context_t& context, std::vector& buffer) override; + + srslog::basic_logger& logger; + uint32_t cc_idx = 0; + uint32_t current_arfcn = 0; + float thr_snr_db = 5.0f; + int serving_cell_pci = -1; + + /// NR-based measuring objects + srsran_ssb_t ssb = {}; ///< SS/PBCH Block +}; + +} // namespace scell +} // namespace srsue + +#endif // SRSRAN_INTRA_MEASURE_NR_H diff --git a/srsue/src/phy/scell/intra_measure_base.cc b/srsue/src/phy/scell/intra_measure_base.cc index f34a63760..9fd8deec5 100644 --- a/srsue/src/phy/scell/intra_measure_base.cc +++ b/srsue/src/phy/scell/intra_measure_base.cc @@ -11,18 +11,10 @@ */ #include "srsue/hdr/phy/scell/intra_measure_base.h" -#define Error(fmt, ...) \ - if (SRSRAN_DEBUG_ENABLED) \ - logger.error("INTRA-%s: " fmt, to_string(get_rat()).c_str(), ##__VA_ARGS__) -#define Warning(fmt, ...) \ - if (SRSRAN_DEBUG_ENABLED) \ - logger.warning("INTRA-%s: " fmt, to_string(get_rat()).c_str(), ##__VA_ARGS__) -#define Info(fmt, ...) \ - if (SRSRAN_DEBUG_ENABLED) \ - logger.info("INTRA-%s: " fmt, to_string(get_rat()).c_str(), ##__VA_ARGS__) -#define Debug(fmt, ...) \ - if (SRSRAN_DEBUG_ENABLED) \ - logger.debug("INTRA-%s: " fmt, to_string(get_rat()).c_str(), ##__VA_ARGS__) +#define Log(level, fmt, ...) \ + do { \ + logger.level("INTRA-%s: " fmt, to_string(get_rat()).c_str(), ##__VA_ARGS__); \ + } while (false) namespace srsue { namespace scell { @@ -64,8 +56,10 @@ void intra_measure_base::init_generic(uint32_t cc_idx_, const args_t& args) } } - state.set_state(internal_state::idle); - start(INTRA_FREQ_MEAS_PRIO); + if (state.get_state() == internal_state::initial) { + state.set_state(internal_state::idle); + start(INTRA_FREQ_MEAS_PRIO); + } } void intra_measure_base::stop() @@ -90,7 +84,7 @@ void intra_measure_base::meas_stop() // Transition state to idle // Ring-buffer shall not be reset, it will automatically be reset as soon as the FSM transitions to receive state.set_state(internal_state::idle); - Info("Disabled neighbour cell search for EARFCN %d", get_earfcn()); + Log(info, "Disabled neighbour cell search for EARFCN %d", get_earfcn()); } void intra_measure_base::set_cells_to_meas(const std::set& pci) @@ -99,16 +93,19 @@ void intra_measure_base::set_cells_to_meas(const std::set& pci) context.active_pci = pci; active_pci_mutex.unlock(); state.set_state(internal_state::receive); - Info("Received list of %zd neighbour cells to measure in EARFCN %d.", pci.size(), get_earfcn()); + Log(info, "Received list of %zd neighbour cells to measure in EARFCN %d.", pci.size(), get_earfcn()); } void intra_measure_base::write(uint32_t tti, cf_t* data, uint32_t nsamples) { + logger.set_context(tti); + int nbytes = (int)(nsamples * sizeof(cf_t)); int required_nbytes = (int)(context.meas_len_ms * context.sf_len * sizeof(cf_t)); uint32_t elapsed_tti = TTI_SUB(tti, last_measure_tti); switch (state.get_state()) { + case internal_state::initial: case internal_state::idle: case internal_state::measure: case internal_state::quit: @@ -119,6 +116,9 @@ void intra_measure_base::write(uint32_t tti, cf_t* data, uint32_t nsamples) state.set_state(internal_state::receive); last_measure_tti = tti; srsran_ringbuffer_reset(&ring_buffer); + + // Force receive state + write(tti, data, nsamples); } break; case internal_state::receive: @@ -127,7 +127,7 @@ void intra_measure_base::write(uint32_t tti, cf_t* data, uint32_t nsamples) // Try writing in the buffer if (srsran_ringbuffer_write(&ring_buffer, data, nbytes) < nbytes) { - Warning("Error writing to ringbuffer (EARFCN=%d)", get_earfcn()); + Log(warning, "Error writing to ringbuffer (EARFCN=%d)", get_earfcn()); // Transition to wait, so it can keep receiving without stopping the component operation state.set_state(internal_state::wait); @@ -146,7 +146,8 @@ void intra_measure_base::measure_proc() std::set cells_to_measure = {}; // Read data from buffer and find cells in it - srsran_ringbuffer_read(&ring_buffer, search_buffer.data(), (int)context.meas_len_ms * context.sf_len * sizeof(cf_t)); + srsran_ringbuffer_read( + &ring_buffer, search_buffer.data(), (int)(context.meas_len_ms * context.sf_len * sizeof(cf_t))); // Go to receive before finishing, so new samples can be enqueued before the thread finishes if (state.get_state() == internal_state::measure) { @@ -166,6 +167,7 @@ void intra_measure_base::run_thread() // Get state internal_state::state_t s = state.get_state(); switch (s) { + case internal_state::initial: case internal_state::idle: case internal_state::wait: case internal_state::receive: diff --git a/srsue/src/phy/scell/intra_measure_lte.cc b/srsue/src/phy/scell/intra_measure_lte.cc index 98dee3701..c3a78bfaf 100644 --- a/srsue/src/phy/scell/intra_measure_lte.cc +++ b/srsue/src/phy/scell/intra_measure_lte.cc @@ -14,18 +14,10 @@ namespace srsue { namespace scell { -#define Error(fmt, ...) \ - if (SRSRAN_DEBUG_ENABLED) \ - logger.error("INTRA-%s: " fmt, to_string(get_rat()).c_str(), ##__VA_ARGS__) -#define Warning(fmt, ...) \ - if (SRSRAN_DEBUG_ENABLED) \ - logger.warning("INTRA-%s: " fmt, to_string(get_rat()).c_str(), ##__VA_ARGS__) -#define Info(fmt, ...) \ - if (SRSRAN_DEBUG_ENABLED) \ - logger.info("INTRA-%s: " fmt, to_string(get_rat()).c_str(), ##__VA_ARGS__) -#define Debug(fmt, ...) \ - if (SRSRAN_DEBUG_ENABLED) \ - logger.debug("INTRA-%s: " fmt, to_string(get_rat()).c_str(), ##__VA_ARGS__) +#define Log(level, fmt, ...) \ + do { \ + logger.level("INTRA-%s: " fmt, to_string(get_rat()).c_str(), ##__VA_ARGS__); \ + } while (false) intra_measure_lte::intra_measure_lte(srslog::basic_logger& logger_, meas_itf& new_cell_itf_) : logger(logger_), scell_rx(logger_), intra_measure_base(logger_, new_cell_itf_) @@ -94,14 +86,15 @@ void intra_measure_lte::measure_rat(const measure_context_t& context, std::vecto m.cfo_hz = refsignal_dl_sync.cfo_Hz; neighbour_cells.push_back(m); - Info("Found neighbour cell: EARFCN=%d, PCI=%03d, RSRP=%5.1f dBm, RSRQ=%5.1f, peak_idx=%5d, " - "CFO=%+.1fHz", - m.earfcn, - m.pci, - m.rsrp, - m.rsrq, - refsignal_dl_sync.peak_index, - refsignal_dl_sync.cfo_Hz); + Log(info, + "Found neighbour cell: EARFCN=%d, PCI=%03d, RSRP=%5.1f dBm, RSRQ=%5.1f, peak_idx=%5d, " + "CFO=%+.1fHz", + m.earfcn, + m.pci, + m.rsrp, + m.rsrq, + refsignal_dl_sync.peak_index, + refsignal_dl_sync.cfo_Hz); } } diff --git a/srsue/src/phy/scell/intra_measure_nr.cc b/srsue/src/phy/scell/intra_measure_nr.cc new file mode 100644 index 000000000..fa4122c01 --- /dev/null +++ b/srsue/src/phy/scell/intra_measure_nr.cc @@ -0,0 +1,122 @@ +/** + * + * \section COPYRIGHT + * + * Copyright 2013-2021 Software Radio Systems Limited + * + * By using this file, you agree to the terms and conditions set + * forth in the LICENSE file which can be found at the top level of + * the distribution. + * + */ +#include "srsue/hdr/phy/scell/intra_measure_nr.h" + +#define Log(level, fmt, ...) \ + do { \ + logger.level("INTRA-%s: " fmt, to_string(get_rat()).c_str(), ##__VA_ARGS__); \ + } while (false) + +namespace srsue { +namespace scell { + +intra_measure_nr::intra_measure_nr(srslog::basic_logger& logger_, meas_itf& new_meas_itf_) : + logger(logger_), intra_measure_base(logger_, new_meas_itf_) +{} + +intra_measure_nr::~intra_measure_nr() +{ + srsran_ssb_free(&ssb); +} + +bool intra_measure_nr::init(uint32_t cc_idx_, const args_t& args) +{ + cc_idx = cc_idx_; + thr_snr_db = args.thr_snr_db; + + // Initialise generic side + intra_measure_base::args_t base_args = {}; + base_args.srate_hz = args.max_srate_hz; + base_args.len_ms = args.max_len_ms; + base_args.period_ms = 20; // Hard-coded, it does not make a difference at this stage + base_args.rx_gain_offset_db = args.rx_gain_offset_dB; + init_generic(cc_idx, base_args); + + // Initialise SSB + srsran_ssb_args_t ssb_args = {}; + ssb_args.max_srate_hz = args.max_srate_hz; + ssb_args.min_scs = args.min_scs; + ssb_args.enable_search = true; + if (srsran_ssb_init(&ssb, &ssb_args) < SRSRAN_SUCCESS) { + Log(error, "Error initiating SSB"); + return false; + } + + return true; +} + +bool intra_measure_nr::set_config(uint32_t arfcn, const config_t& cfg) +{ + // Update ARFCN + current_arfcn = arfcn; + serving_cell_pci = cfg.serving_cell_pci; + + // Configure generic side + intra_measure_base::args_t base_cfg = {}; + base_cfg.srate_hz = cfg.srate_hz; + base_cfg.len_ms = cfg.len_ms; + base_cfg.period_ms = cfg.periodicity_ms; + base_cfg.rx_gain_offset_db = cfg.rx_gain_offset_db; + init_generic(cc_idx, base_cfg); + + // Configure SSB + srsran_ssb_cfg_t ssb_cfg = {}; + ssb_cfg.srate_hz = cfg.srate_hz; + ssb_cfg.center_freq_hz = cfg.center_freq_hz; + ssb_cfg.ssb_freq_hz = cfg.ssb_freq_hz; + ssb_cfg.scs = cfg.scs; + if (srsran_ssb_set_cfg(&ssb, &ssb_cfg) < SRSRAN_SUCCESS) { + Log(error, "Error configuring SSB"); + return false; + } + + return true; +} + +void intra_measure_nr::measure_rat(const measure_context_t& context, std::vector& buffer) +{ + // Search and measure the best cell + srsran_csi_trs_measurements_t meas = {}; + uint32_t N_id = 0; + if (srsran_ssb_csi_search(&ssb, buffer.data(), context.sf_len * context.meas_len_ms, &N_id, &meas) < SRSRAN_SUCCESS) { + Log(error, "Error searching for SSB"); + } + + // Early return if the found PCI matches with the serving cell ID + if (serving_cell_pci == (int)N_id) { + return; + } + + // Check threshold + if (meas.snr_dB >= thr_snr_db) { + // Log finding + if (logger.info.enabled()) { + std::array str_info = {}; + srsran_csi_rs_measure_info(&meas, str_info.data(), (uint32_t)str_info.size()); + Log(info, "Found neighbour cell: ARFCN=%d PCI=%03d %s", get_earfcn(), N_id, str_info.data()); + } + + // Prepare found measurements + std::vector meas_list(1); + meas_list[0].rat = get_rat(); + meas_list[0].rsrp = meas.rsrp_dB + context.rx_gain_offset_db; + meas_list[0].cfo_hz = meas.cfo_hz; + meas_list[0].earfcn = get_earfcn(); + meas_list[0].pci = N_id; + + // Push measurements to higher layers + context.new_cell_itf.new_cell_meas(cc_idx, meas_list); + } +} + +} // namespace scell +} // namespace srsue \ No newline at end of file diff --git a/srsue/test/phy/CMakeLists.txt b/srsue/test/phy/CMakeLists.txt index 8656910d6..c635b86c3 100644 --- a/srsue/test/phy/CMakeLists.txt +++ b/srsue/test/phy/CMakeLists.txt @@ -39,3 +39,13 @@ target_link_libraries(scell_search_test ${CMAKE_THREAD_LIBS_INIT} ${Boost_LIBRARIES}) add_lte_test(scell_search_test scell_search_test --duration=5 --cell.nof_prb=6 --active_cell_list=2,3,4,5,6 --simulation_cell_list=1,2,3,4,5,6 --channel_period_s=30 --channel.hst.fd=750 --channel.delay_max=10000) + +add_executable(nr_cell_search_test nr_cell_search_test.cc) +target_link_libraries(nr_cell_search_test + srsue_phy + srsran_common + srsran_phy + srsran_radio + ${CMAKE_THREAD_LIBS_INIT} + ${Boost_LIBRARIES}) +add_nr_test(nr_cell_search_test nr_cell_search_test) \ No newline at end of file diff --git a/srsue/test/phy/nr_cell_search_test.cc b/srsue/test/phy/nr_cell_search_test.cc new file mode 100644 index 000000000..70a8b3371 --- /dev/null +++ b/srsue/test/phy/nr_cell_search_test.cc @@ -0,0 +1,453 @@ +/** + * + * \section COPYRIGHT + * + * Copyright 2013-2021 Software Radio Systems Limited + * + * By using this file, you agree to the terms and conditions set + * forth in the LICENSE file which can be found at the top level of + * the distribution. + * + */ + +#include "srsran/common/band_helper.h" +#include "srsran/common/string_helpers.h" +#include "srsran/common/test_common.h" +#include "srsran/interfaces/phy_interface_types.h" +#include "srsran/radio/radio.h" +#include "srsran/srslog/srslog.h" +#include "srsue/hdr/phy/scell/intra_measure_nr.h" +#include +#include +#include +#include +#include +#include + +// Test gNb class +class test_gnb +{ +private: + uint32_t pci; + srsran_ssb_t ssb = {}; + std::vector signal_buffer = {}; + srslog::basic_logger& logger; + +public: + struct args_t { + uint32_t pci = 500; + double srate_hz = 11.52e6; + double center_freq_hz = 3.5e9; + double ssb_freq_hz = 3.5e9 - 960e3; + srsran_subcarrier_spacing_t ssb_scs = srsran_subcarrier_spacing_30kHz; + uint32_t ssb_period_ms = 20; + uint16_t band; + + srsran_ssb_patern_t get_ssb_pattern() const { return srsran::srsran_band_helper().get_ssb_pattern(band, ssb_scs); } + srsran_duplex_mode_t get_duplex_mode() const { return srsran::srsran_band_helper().get_duplex_mode(band); } + }; + + test_gnb(const args_t& args) : logger(srslog::fetch_basic_logger("PCI=" + std::to_string(args.pci))) + { + // Initialise internals + pci = args.pci; + + // Initialise SSB + srsran_ssb_args_t ssb_args = {}; + ssb_args.max_srate_hz = args.srate_hz; + ssb_args.min_scs = args.ssb_scs; + ssb_args.enable_encode = true; + if (srsran_ssb_init(&ssb, &ssb_args) < SRSRAN_SUCCESS) { + logger.error("Error initialising SSB"); + return; + } + + // Configure SSB + srsran_ssb_cfg_t ssb_cfg = {}; + ssb_cfg.srate_hz = args.srate_hz; + ssb_cfg.center_freq_hz = args.center_freq_hz; + ssb_cfg.ssb_freq_hz = args.ssb_freq_hz; + ssb_cfg.scs = args.ssb_scs; + ssb_cfg.pattern = args.get_ssb_pattern(); + ssb_cfg.position[0] = true; + ssb_cfg.duplex_mode = args.get_duplex_mode(); + ssb_cfg.periodicity_ms = args.ssb_period_ms; + if (srsran_ssb_set_cfg(&ssb, &ssb_cfg) < SRSRAN_SUCCESS) { + logger.error("Error configuring SSB"); + return; + } + } + + int work(uint32_t sf_idx, std::vector& baseband_buffer) + { + logger.set_context(sf_idx); + + // Check if SSB needs to be sent + if (srsran_ssb_send(&ssb, sf_idx)) { + // Prepare PBCH message + srsran_pbch_msg_nr_t msg = {}; + + // Add SSB + if (srsran_ssb_add(&ssb, pci, &msg, baseband_buffer.data(), baseband_buffer.data()) < SRSRAN_SUCCESS) { + logger.error("Error adding SSB"); + return SRSRAN_ERROR; + } + } + + return SRSRAN_SUCCESS; + } + + ~test_gnb() { srsran_ssb_free(&ssb); } +}; + +struct args_t { + // Common execution parameters + uint32_t duration_s = 1; + uint32_t nof_prb = 52; + std::string log_level = "info"; + std::string active_cell_list = "500"; + std::string simulation_cell_list = "500"; + uint32_t meas_len_ms = 1; + uint32_t meas_period_ms = 20; + uint32_t carier_arfcn = 634240; + uint32_t ssb_arfcn = 634176; + srsran_subcarrier_spacing_t carrier_scs = srsran_subcarrier_spacing_15kHz; + srsran_subcarrier_spacing_t ssb_scs = srsran_subcarrier_spacing_30kHz; + float thr_snr_db = 5.0f; + + // On the Fly parameters + std::string radio_device_name = "auto"; + std::string radio_device_args = "auto"; + std::string radio_log_level = "info"; + float rx_gain = 60.0f; + + // Parsed PCI lists + std::set pcis_to_meas; + std::set pcis_to_simulate; +}; + +class meas_itf_listener : public srsue::scell::intra_measure_base::meas_itf +{ +public: + typedef struct { + float rsrp_avg; + float rsrp_min; + float rsrp_max; + float rsrq_avg; + float rsrq_min; + float rsrq_max; + uint32_t count; + } cell_meas_t; + + std::map cells; + + void cell_meas_reset(uint32_t cc_idx) override {} + void new_cell_meas(uint32_t cc_idx, const std::vector& meas) override + { + for (auto& m : meas) { + uint32_t pci = m.pci; + if (!cells.count(pci)) { + cells[pci].rsrp_min = m.rsrp; + cells[pci].rsrp_max = m.rsrp; + cells[pci].rsrp_avg = m.rsrp; + cells[pci].rsrq_min = m.rsrq; + cells[pci].rsrq_max = m.rsrq; + cells[pci].rsrq_avg = m.rsrq; + cells[pci].count = 1; + } else { + cells[pci].rsrp_min = SRSRAN_MIN(cells[pci].rsrp_min, m.rsrp); + cells[pci].rsrp_max = SRSRAN_MAX(cells[pci].rsrp_max, m.rsrp); + cells[pci].rsrp_avg = (m.rsrp + cells[pci].rsrp_avg * cells[pci].count) / (cells[pci].count + 1); + + cells[pci].rsrq_min = SRSRAN_MIN(cells[pci].rsrq_min, m.rsrq); + cells[pci].rsrq_max = SRSRAN_MAX(cells[pci].rsrq_max, m.rsrq); + cells[pci].rsrq_avg = (m.rsrq + cells[pci].rsrq_avg * cells[pci].count) / (cells[pci].count + 1); + cells[pci].count++; + } + } + } + + bool print_stats(args_t args) + { + printf("\n-- Statistics:\n"); + uint32_t true_counts = 0; + uint32_t false_counts = 0; + uint32_t tti_count = (1000 * args.duration_s) / args.meas_period_ms; + uint32_t ideal_true_counts = args.pcis_to_simulate.size() * tti_count; + uint32_t ideal_false_counts = tti_count * cells.size() - ideal_true_counts; + + for (auto& e : cells) { + bool false_alarm = args.pcis_to_simulate.find(e.first) == args.pcis_to_simulate.end(); + + if (false_alarm) { + false_counts += e.second.count; + } else { + true_counts += e.second.count; + } + + printf(" pci=%03d; count=%3d; false=%s; rsrp=%+.1f|%+.1f|%+.1fdBfs; rsrq=%+.1f|%+.1f|%+.1fdB;\n", + e.first, + e.second.count, + false_alarm ? "y" : "n", + e.second.rsrp_min, + e.second.rsrp_avg, + e.second.rsrp_max, + e.second.rsrq_min, + e.second.rsrq_avg, + e.second.rsrq_max); + } + + float prob_detection = (ideal_true_counts) ? (float)true_counts / (float)ideal_true_counts : 0.0f; + float prob_false_alarm = (ideal_false_counts) ? (float)false_counts / (float)ideal_false_counts : 0.0f; + printf("\n"); + printf(" Probability of detection: %.6f\n", prob_detection); + printf(" Probability of false alarm: %.6f\n", prob_false_alarm); + + return (prob_detection >= 0.9f && prob_false_alarm <= 0.1f); + } +}; + +// shorten boost program options namespace +namespace bpo = boost::program_options; + +int parse_args(int argc, char** argv, args_t& args) +{ + int ret = SRSRAN_SUCCESS; + + bpo::options_description options; + bpo::options_description common("Measurement options"); + bpo::options_description over_the_air("Over the air options"); + bpo::options_description simulation("Simulation execution options"); + + // clang-format off + common.add_options() + ("duration", bpo::value(&args.duration_s), "Duration of the test in seconds") + ("nof_prb", bpo::value(&args.nof_prb), "Cell Number of PRB") + ("log_level", bpo::value(&args.log_level), "Intra measurement log level (none, warning, info, debug)") + ("meas_len_ms", bpo::value(&args.meas_len_ms), "Measurement length") + ("meas_period_ms", bpo::value(&args.meas_period_ms), "Measurement period") + ("active_cell_list", bpo::value(&args.active_cell_list), "Comma separated PCI cell list to measure") + ("carrier_arfcn", bpo::value(&args.carier_arfcn), "Carrier center frequency ARFCN") + ("ssb_arfcn", bpo::value(&args.ssb_arfcn), "SSB center frequency in ARFCN") + ("thr_snr_db", bpo::value(&args.thr_snr_db), "Detection threshold for SNR in dB") + ; + + over_the_air.add_options() + ("rf.device_name", bpo::value(&args.radio_device_name), "RF Device Name") + ("rf.device_args", bpo::value(&args.radio_device_args), "RF Device arguments") + ("rf.log_level", bpo::value(&args.radio_log_level), "RF Log level (none, warning, info, debug)") + ("rf.rx_gain", bpo::value(&args.rx_gain), "RF Receiver gain in dB") + ; + + simulation.add_options() + ("simulation_cell_list", bpo::value(&args.simulation_cell_list), "Comma separated PCI cell list to simulate") + ; + + options.add(common).add(over_the_air).add(simulation).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 = SRSRAN_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 = SRSRAN_ERROR; + } + + return ret; +} + +static void pci_list_parse_helper(std::string& list_str, std::set& list) +{ + if (list_str == "all") { + // Add all possible cells + for (int i = 0; i < SRSRAN_NOF_NID_NR; i++) { + list.insert(i); + } + } else if (list_str == "none") { + list.clear(); + } else if (not list_str.empty()) { + // Remove spaces from neightbour cell list + list_str = srsran::string_remove_char(list_str, ' '); + + // Add cell to known cells + srsran::string_parse_list(list_str, ',', list); + } +} + +int main(int argc, char** argv) +{ + int ret; + + // Parse args + args_t args = {}; + if (parse_args(argc, argv, args) < SRSRAN_SUCCESS) { + return SRSRAN_ERROR; + } + + // Deduce base-band parameters + uint32_t sf_len = srsran_min_symbol_sz_rb(args.nof_prb) * SRSRAN_SUBC_SPACING_NR(args.carrier_scs) / 1000U; + double srate_hz = (double)sf_len * 1000.0; + double center_freq_hz = srsran::srsran_band_helper().nr_arfcn_to_freq(args.carier_arfcn); + double ssb_freq_hz = srsran::srsran_band_helper().nr_arfcn_to_freq(args.ssb_arfcn); + uint16_t band = srsran::srsran_band_helper().get_band_from_dl_freq_Hz(center_freq_hz); + + // Allocate buffer + std::vector baseband_buffer(sf_len); + + // Initiate logging + srslog::init(); + srslog::basic_logger& logger = srslog::fetch_basic_logger("PHY"); + logger.set_level(srslog::str_to_basic_level(args.log_level)); + + // Create measurement callback + meas_itf_listener rrc; + + // Create measurement instance + srsue::scell::intra_measure_nr intra_measure(logger, rrc); + + // Initialise measurement instance + srsue::scell::intra_measure_nr::args_t meas_args = {}; + meas_args.rx_gain_offset_dB = 0.0f; + meas_args.max_len_ms = args.meas_len_ms; + meas_args.max_srate_hz = srate_hz; + meas_args.min_scs = args.ssb_scs; + meas_args.thr_snr_db = args.thr_snr_db; + TESTASSERT(intra_measure.init(0, meas_args)); + + // Setup measurement + srsue::scell::intra_measure_nr::config_t meas_cfg = {}; + meas_cfg.arfcn = args.carier_arfcn; + meas_cfg.srate_hz = srate_hz; + meas_cfg.len_ms = args.meas_len_ms; + meas_cfg.periodicity_ms = args.meas_period_ms; + meas_cfg.rx_gain_offset_db = 0; + meas_cfg.center_freq_hz = center_freq_hz; + meas_cfg.ssb_freq_hz = ssb_freq_hz; + meas_cfg.scs = srsran_subcarrier_spacing_30kHz; + meas_cfg.serving_cell_pci = -1; + TESTASSERT(intra_measure.set_config(args.carier_arfcn, meas_cfg)); + + // Simulation only + std::vector > test_gnb_v; + + // Over-the-air only + std::unique_ptr radio = nullptr; + + // Parse PCI lists + pci_list_parse_helper(args.active_cell_list, args.pcis_to_meas); + pci_list_parse_helper(args.simulation_cell_list, args.pcis_to_simulate); + + // Setup raio if the list of PCIs to simulate is empty + if (args.pcis_to_simulate.empty()) { + // Create radio log + auto& radio_logger = srslog::fetch_basic_logger("RF", false); + radio_logger.set_level(srslog::str_to_basic_level(args.radio_log_level)); + + // Create radio + radio = std::unique_ptr(new srsran::radio); + + // Init radio + srsran::rf_args_t radio_args = {}; + radio_args.device_args = args.radio_device_args; + radio_args.device_name = args.radio_device_name; + radio_args.nof_carriers = 1; + radio_args.nof_antennas = 1; + radio->init(radio_args, nullptr); + + // Set sampling rate + radio->set_rx_srate(srate_hz); + + // Set frequency + radio->set_rx_freq(0, center_freq_hz); + + } else { + // Create test eNb's if radio is not available + for (const uint32_t& pci : args.pcis_to_simulate) { + // Initialise channel and push back + test_gnb::args_t gnb_args = {}; + gnb_args.pci = pci; + gnb_args.srate_hz = srate_hz; + gnb_args.center_freq_hz = center_freq_hz; + gnb_args.ssb_freq_hz = ssb_freq_hz; + gnb_args.ssb_scs = args.ssb_scs; + gnb_args.ssb_period_ms = args.meas_period_ms; + gnb_args.band = band; + test_gnb_v.push_back(std::unique_ptr(new test_gnb(gnb_args))); + + // Add cell to known cells + if (args.active_cell_list.empty()) { + args.pcis_to_meas.insert(pci); + } + } + } + + // pass cells to measure to intra_measure object + intra_measure.set_cells_to_meas(args.pcis_to_meas); + + // Run loop + for (uint32_t sf_idx = 0; sf_idx < args.duration_s * 1000; sf_idx++) { + logger.set_context(sf_idx); + srsran::rf_timestamp_t ts = {}; + + // Clean buffer + srsran_vec_cf_zero(baseband_buffer.data(), sf_len); + + if (radio) { + // Receive radio + srsran::rf_buffer_t radio_buffer(baseband_buffer.data(), sf_len); + radio->rx_now(radio_buffer, ts); + } else { + // Run gNb simulator + for (auto& gnb : test_gnb_v) { + gnb->work(sf_idx, baseband_buffer); + } + + // if it measuring, wait for avoiding overflowing + intra_measure.wait_meas(); + } + + // Increase Time counter + ts.add(0.001); + + // Give data to intra measure component + intra_measure.write(sf_idx % 10240, baseband_buffer.data(), sf_len); + if (sf_idx % 1000 == 0) { + logger.info("Done %.1f%%\n", (double)sf_idx * 100.0 / ((double)args.duration_s * 1000.0)); + } + } + + // make sure last measurement has been received before stopping + if (not radio) { + intra_measure.wait_meas(); + } + + // Stop, it will block until the asynchronous thread quits + intra_measure.stop(); + + ret = rrc.print_stats(args) ? SRSRAN_SUCCESS : SRSRAN_ERROR; + + if (radio) { + radio->stop(); + } + + srslog::flush(); + + if (ret && radio == nullptr) { + printf("Error\n"); + } else { + printf("Ok\n"); + } + + return ret; +}