diff --git a/CMakeLists.txt b/CMakeLists.txt index 95232c3915..b78b50e5f1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,7 @@ cmake_minimum_required (VERSION 3.16) project (OpenAirInterface LANGUAGES C CXX) +set(CMAKE_CXX_STANDARD 17) ######################################################### # Base directories, compatible with legacy OAI building # @@ -30,6 +31,10 @@ project (OpenAirInterface LANGUAGES C CXX) set (OPENAIR_DIR ${CMAKE_SOURCE_DIR}) include("cmake_targets/macros.cmake") +if(NOT DEFINED ENV{CPM_SOURCE_CACHE}) + set(CPM_SOURCE_CACHE ~/.cache/cpm/) +endif() +include("cmake_targets/CPM.cmake") ############################## ### CCache: reduce compilation time @@ -288,6 +293,8 @@ set_property(CACHE KPM_VERSION PROPERTY STRINGS "KPM_V2_03" "KPM_V3_00") message(STATUS "Selected KPM Version: ${KPM_VERSION}") +add_boolean_option(ENABLE_IMSCOPE OFF "Enable phy scope based on imgui" ON) + ################################################## # ASN.1 grammar C code generation & dependencies # ################################################## @@ -2339,30 +2346,24 @@ if(ENABLE_TESTS) find_package(GTest) if (NOT GTest_FOUND) message(STATUS "GTest package not found, will download googletest automatically. To prevent that install google test on your system (libgtest-dev)") - include(FetchContent) - FetchContent_Declare( - googletest - GIT_REPOSITORY https://github.com/google/googletest.git - GIT_TAG 58d77fa8070e8cec2dc1ed015d66b454c8d78850 # 1.12.1 + CPMAddPackage( + NAME googletest + GITHUB_REPOSITORY google/googletest + GIT_TAG release-1.12.1 + VERSION 1.12.1 + OPTIONS "INSTALL_GTEST OFF" "gtest_force_shared_crt" "BUILD_GMOCK OFF" ) - set(BUILD_GMOCK OFF) - set(INSTALL_GTEST OFF) - FetchContent_MakeAvailable(googletest) - add_library(GTest::gtest ALIAS gtest) - add_library(GTest::gtest_main ALIAS gtest_main) endif() find_package(benchmark) if (NOT benchmark_FOUND) message(STATUS "benchmark package not found, will download benchmark automatically. To prevent that install google benchmark on your system (libbenchmark-dev)") - include(FetchContent) - set(BENCHMARK_ENABLE_TESTING OFF) - FetchContent_Declare( - benchmark - GIT_REPOSITORY https://github.com/google/benchmark.git - GIT_TAG v1.9.0 + CPMAddPackage( + NAME benchmark + GITHUB_REPOSITORY google/benchmark + VERSION 1.9.0 + OPTIONS "BENCHMARK_ENABLE_TESTING OFF" ) - FetchContent_MakeAvailable(benchmark) endif() endif() diff --git a/cmake_targets/CPM.cmake b/cmake_targets/CPM.cmake new file mode 100644 index 0000000000..20013e1870 --- /dev/null +++ b/cmake_targets/CPM.cmake @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: MIT +# +# SPDX-FileCopyrightText: Copyright (c) 2019-2023 Lars Melchior and contributors + +set(CPM_DOWNLOAD_VERSION 0.40.1) +set(CPM_HASH_SUM "117cbf2711572f113bab262933eb5187b08cfc06dce0714a1ee94f2183ddc3ec") + +if(CPM_SOURCE_CACHE) + set(CPM_DOWNLOAD_LOCATION "${CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +elseif(DEFINED ENV{CPM_SOURCE_CACHE}) + set(CPM_DOWNLOAD_LOCATION "$ENV{CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +else() + set(CPM_DOWNLOAD_LOCATION "${CMAKE_BINARY_DIR}/cmake/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +endif() + +# Expand relative path. This is important if the provided path contains a tilde (~) +get_filename_component(CPM_DOWNLOAD_LOCATION ${CPM_DOWNLOAD_LOCATION} ABSOLUTE) + +file(DOWNLOAD + https://github.com/cpm-cmake/CPM.cmake/releases/download/v${CPM_DOWNLOAD_VERSION}/CPM.cmake + ${CPM_DOWNLOAD_LOCATION} EXPECTED_HASH SHA256=${CPM_HASH_SUM} +) + +include(${CPM_DOWNLOAD_LOCATION}) diff --git a/cmake_targets/build_oai b/cmake_targets/build_oai index cd7e326be8..1d04174aea 100755 --- a/cmake_targets/build_oai +++ b/cmake_targets/build_oai @@ -46,7 +46,7 @@ BUILD_DOXYGEN=0 DISABLE_HARDWARE_DEPENDENCY="False" CMAKE_BUILD_TYPE="RelWithDebInfo" CMAKE_CMD="$CMAKE" -OPTIONAL_LIBRARIES="telnetsrv enbscope uescope nrscope ldpc_cuda ldpc_t2 websrv oai_iqplayer" +OPTIONAL_LIBRARIES="telnetsrv enbscope uescope nrscope ldpc_cuda ldpc_t2 websrv oai_iqplayer imscope" TARGET_LIST="" function print_help() { diff --git a/common/utils/nr/nr_common.h b/common/utils/nr/nr_common.h index fcefa29461..cafeebb632 100644 --- a/common/utils/nr/nr_common.h +++ b/common/utils/nr/nr_common.h @@ -254,6 +254,9 @@ int get_smallest_supported_bandwidth_index(int scs, frequency_range_t frequency_ #define CEILIDIV(a,b) ((a+b-1)/b) #define ROUNDIDIV(a,b) (((a<<1)+b)/(b<<1)) +// Align up to a multiple of 16 +#define ALIGN_UP_16(a) ((a + 15) & ~15) + #ifdef __cplusplus #ifdef min #undef min diff --git a/doc/BUILD.md b/doc/BUILD.md index 3e06e79d01..dc5b1361f2 100644 --- a/doc/BUILD.md +++ b/doc/BUILD.md @@ -42,6 +42,8 @@ Running the [build_oai](../cmake_targets/build_oai) script also generates some The build system for OAI uses [cmake](https://cmake.org/) which is a tool to generate makefiles. The `build_oai` script is a wrapper using `cmake` and `make`/`ninja` to ease the oai build and use. It logs the `cmake` and `ninja`/`make` commands it executes. The file describing how to build the executables from source files is the [CMakeLists.txt](../CMakeLists.txt), it is used as input by cmake to generate the makefiles. +cmake is further extended by using [CPM](https://github.com/cpm-cmake/CPM.cmake). CPM is a cmake script that handles external code dependencies. It is setup to cache downloaded code in `~/.cache/cpm`. While most external dependencies should be handled by system package managers, CPM has the advantage of using any code that is available in a public git repository. + The oai softmodem supports many use cases, and new ones are regularly added. Most of them are accessible using the configuration file or the command line options and continuous effort is done to avoid introducing build options as it makes tests and usage more complicated than run-time options. The following functionalities, originally requiring a specific build are now accessible by configuration or command line options: - s1, noS1 diff --git a/doc/README.md b/doc/README.md index 636c30d26b..4b05242e59 100644 --- a/doc/README.md +++ b/doc/README.md @@ -106,7 +106,7 @@ The other SDRs (AW2S, LimeSDR, ...) have no READMEs. ## Special-purpose libraries -- OAI has a scope based on Xforms, described in [this README](../openair1/PHY/TOOLS/readme.md) +- OAI has two scopes: one based on Xforms and one based on imgui, described in [this README](../openair1/PHY/TOOLS/readme.md) - OAI comes with an integrated [telnet server](../common/utils/telnetsrv/DOC/telnethelp.md) to monitor and control - OAI comes with an integrated [web server](../common/utils/websrv/DOC/websrv.md) diff --git a/executables/nr-softmodem.c b/executables/nr-softmodem.c index 6f49537b3a..c28b741406 100644 --- a/executables/nr-softmodem.c +++ b/executables/nr-softmodem.c @@ -734,14 +734,19 @@ int main( int argc, char **argv ) { printf("ALL RUs ready - init gNBs\n"); for (int idx=0;idxif_inst->sl_ahead = sl_ahead; - if(IS_SOFTMODEM_DOSCOPE) { + if (IS_SOFTMODEM_DOSCOPE || IS_SOFTMODEM_IMSCOPE_ENABLED) { sleep(1); scopeParms_t p; - p.argc=&argc; - p.argv=argv; - p.gNB=RC.gNB[0]; - p.ru=RC.ru[0]; - load_softscope("nr",&p); + p.argc = &argc; + p.argv = argv; + p.gNB = RC.gNB[0]; + p.ru = RC.ru[0]; + if (IS_SOFTMODEM_DOSCOPE) { + load_softscope("nr", &p); + } + if (IS_SOFTMODEM_IMSCOPE_ENABLED) { + load_softscope("im", &p); + } } if (NFAPI_MODE != NFAPI_MODE_PNF && NFAPI_MODE != NFAPI_MODE_VNF && NFAPI_MODE != NFAPI_MODE_AERIAL) { diff --git a/executables/nr-uesoftmodem.c b/executables/nr-uesoftmodem.c index 5049e269d2..99ce9040d4 100644 --- a/executables/nr-uesoftmodem.c +++ b/executables/nr-uesoftmodem.c @@ -521,8 +521,11 @@ int main(int argc, char **argv) init_openair0(); lock_memory_to_ram(); - if(IS_SOFTMODEM_DOSCOPE) { - load_softscope("nr",PHY_vars_UE_g[0][0]); + if (IS_SOFTMODEM_DOSCOPE) { + load_softscope("nr", PHY_vars_UE_g[0][0]); + } + if (IS_SOFTMODEM_IMSCOPE_ENABLED) { + load_softscope("im", PHY_vars_UE_g[0][0]); } for (int inst = 0; inst < NB_UE_INST; inst++) { diff --git a/executables/softmodem-bits.h b/executables/softmodem-bits.h new file mode 100644 index 0000000000..6d527148cd --- /dev/null +++ b/executables/softmodem-bits.h @@ -0,0 +1,61 @@ +/* + * Licensed to the OpenAirInterface (OAI) Software Alliance under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The OpenAirInterface Software Alliance licenses this file to You under + * the OAI Public License, Version 1.1 (the "License"); you may not use this file + * except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.openairinterface.org/?page_id=698 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *------------------------------------------------------------------------------- + * For more information about the OpenAirInterface (OAI) Software Alliance: + * contact@openairinterface.org + */ + +#ifndef SOFTMODEM_BITS_H +#define SOFTMODEM_BITS_H + +// clang-format off +#define BIT_0 (1 << 0) +#define BIT_1 (1 << 1) +#define BIT_2 (1 << 2) +#define BIT_10 (1 << 10) +#define BIT_12 (1 << 12) +#define BIT_13 (1 << 13) +#define BIT_15 (1 << 15) +#define BIT_16 (1 << 16) +#define BIT_17 (1 << 17) +#define BIT_18 (1 << 18) +#define BIT_20 (1 << 20) +#define BIT_21 (1 << 21) +#define BIT_22 (1 << 22) +#define BIT_23 (1 << 23) +#define BIT_24 (1 << 24) +#define BIT_25 (1 << 25) + +#define SOFTMODEM_NOS1_BIT BIT_0 +#define SOFTMODEM_NOKRNMOD_BIT BIT_1 +#define SOFTMODEM_NONBIOT_BIT BIT_2 +#define SOFTMODEM_RFSIM_BIT BIT_10 +#define SOFTMODEM_SIML1_BIT BIT_12 +#define SOFTMODEM_DLSIM_BIT BIT_13 +#define SOFTMODEM_DOSCOPE_BIT BIT_15 +#define SOFTMODEM_RECPLAY_BIT BIT_16 +#define SOFTMODEM_TELNETCLT_BIT BIT_17 +#define SOFTMODEM_RECRECORD_BIT BIT_18 +#define SOFTMODEM_ENB_BIT BIT_20 +#define SOFTMODEM_GNB_BIT BIT_21 +#define SOFTMODEM_4GUE_BIT BIT_22 +#define SOFTMODEM_5GUE_BIT BIT_23 +#define SOFTMODEM_NOSTATS_BIT BIT_24 +#define SOFTMODEM_IMSCOPE_BIT BIT_25 +// clang-format on + +#endif // SOFTMODEM_BITS_H diff --git a/executables/softmodem-common.c b/executables/softmodem-common.c index be2a79eb64..d566767902 100644 --- a/executables/softmodem-common.c +++ b/executables/softmodem-common.c @@ -99,6 +99,7 @@ void get_common_options(configmodule_interface_t *cfg, uint32_t execmask) uint32_t start_websrv = 0; uint32_t noS1 = 0, nonbiot = 0; uint32_t rfsim = 0, do_forms = 0; + uint32_t enable_imscope = 0; int nfapi_index = 0; char *logmem_filename = NULL; check_execmask(execmask); @@ -158,6 +159,10 @@ void get_common_options(configmodule_interface_t *cfg, uint32_t execmask) set_softmodem_optmask(SOFTMODEM_DOSCOPE_BIT); } + if (enable_imscope) { + set_softmodem_optmask(SOFTMODEM_IMSCOPE_BIT); + } + if (start_websrv) { load_module_shlib("websrv", NULL, 0, NULL); } diff --git a/executables/softmodem-common.h b/executables/softmodem-common.h index 110a5c60a7..141afa1a37 100644 --- a/executables/softmodem-common.h +++ b/executables/softmodem-common.h @@ -32,6 +32,7 @@ #ifndef SOFTMODEM_COMMON_H #define SOFTMODEM_COMMON_H #include "openair1/PHY/defs_common.h" +#include "softmodem-bits.h" #ifdef __cplusplus extern "C" { @@ -76,6 +77,7 @@ extern "C" #define CONFIG_HLP_256QAM "Use the 256 QAM mcs table for PDSCH\n" #define CONFIG_HLP_CHESTFREQ "Set channel estimation type in frequency domain. 0-Linear interpolation (default). 1-PRB based averaging of channel estimates in frequency. \n" #define CONFIG_HLP_CHESTTIME "Set channel estimation type in time domain. 0-Symbols take estimates of the last preceding DMRS symbol (default). 1-Symbol based averaging of channel estimates in time. \n" +#define CONFIG_HLP_IMSCOPE "Enable phy scope based on imgui and implot" #define CONFIG_HLP_NONSTOP "Go back to frame sync mode after 100 consecutive PBCH failures\n" //#define CONFIG_HLP_NUMUES "Set the number of UEs for the emulation" @@ -188,6 +190,7 @@ extern int usrp_tx_thread; {"sync-ref", CONFIG_HLP_SYNC_REF, 0, .uptr=&SYNC_REF, .defintval=0, TYPE_UINT, 0}, \ {"A" , CONFIG_HLP_TADV, 0, .iptr=&softmodem_params.command_line_sample_advance,.defintval=0, TYPE_INT, 0}, \ {"E" , CONFIG_HLP_TQFS, PARAMFLAG_BOOL, .iptr=&softmodem_params.threequarter_fs, .defintval=0, TYPE_INT, 0}, \ + {"imscope" , CONFIG_HLP_IMSCOPE, PARAMFLAG_BOOL, .uptr=&enable_imscope, .defintval=0, TYPE_UINT, 0}, \ } // clang-format on @@ -235,6 +238,7 @@ extern int usrp_tx_thread; { .s5 = { NULL } }, \ { .s5 = { NULL } }, \ { .s5 = { NULL } }, \ + { .s5 = { NULL } }, \ } // clang-format on @@ -274,21 +278,6 @@ extern int usrp_tx_thread; /***************************************************************************************************************************************/ -#define SOFTMODEM_NOS1_BIT (1<<0) -#define SOFTMODEM_NONBIOT_BIT (1<<2) -#define SOFTMODEM_RFSIM_BIT (1<<10) -#define SOFTMODEM_SIML1_BIT (1<<12) -#define SOFTMODEM_DLSIM_BIT (1<<13) -#define SOFTMODEM_DOSCOPE_BIT (1<<15) -#define SOFTMODEM_RECPLAY_BIT (1<<16) -#define SOFTMODEM_TELNETCLT_BIT (1<<17) -#define SOFTMODEM_RECRECORD_BIT (1<<18) -#define SOFTMODEM_ENB_BIT (1<<20) -#define SOFTMODEM_GNB_BIT (1<<21) -#define SOFTMODEM_4GUE_BIT (1<<22) -#define SOFTMODEM_5GUE_BIT (1<<23) -#define SOFTMODEM_NOSTATS_BIT (1<<24) - #define SOFTMODEM_FUNC_BITS (SOFTMODEM_ENB_BIT | SOFTMODEM_GNB_BIT | SOFTMODEM_5GUE_BIT | SOFTMODEM_4GUE_BIT) #define MAPPING_SOFTMODEM_FUNCTIONS {{"enb",SOFTMODEM_ENB_BIT},{"gnb",SOFTMODEM_GNB_BIT},{"4Gue",SOFTMODEM_4GUE_BIT},{"5Gue",SOFTMODEM_5GUE_BIT}} @@ -307,6 +296,7 @@ extern int usrp_tx_thread; #define IS_SOFTMODEM_4GUE_BIT ( get_softmodem_optmask() & SOFTMODEM_4GUE_BIT) #define IS_SOFTMODEM_5GUE_BIT ( get_softmodem_optmask() & SOFTMODEM_5GUE_BIT) #define IS_SOFTMODEM_NOSTATS_BIT ( get_softmodem_optmask() & SOFTMODEM_NOSTATS_BIT) +#define IS_SOFTMODEM_IMSCOPE_ENABLED ( get_softmodem_optmask() & SOFTMODEM_IMSCOPE_BIT) typedef struct { uint64_t optmask; diff --git a/openair1/PHY/INIT/nr_init.c b/openair1/PHY/INIT/nr_init.c index d47eec2278..68333c821c 100644 --- a/openair1/PHY/INIT/nr_init.c +++ b/openair1/PHY/INIT/nr_init.c @@ -199,7 +199,7 @@ int phy_init_nr_gNB(PHY_VARS_gNB *gNB) int n_buf = Prx*max_ul_mimo_layers; int nb_re_pusch = N_RB_UL * NR_NB_SC_PER_RB; - int nb_re_pusch2 = (nb_re_pusch + 15) & ~15; + int nb_re_pusch2 = ALIGN_UP_16(nb_re_pusch); gNB->pusch_vars = (NR_gNB_PUSCH *)malloc16_clear(gNB->max_nb_pusch * sizeof(NR_gNB_PUSCH)); for (int ULSCH_id = 0; ULSCH_id < gNB->max_nb_pusch; ULSCH_id++) { diff --git a/openair1/PHY/NR_TRANSPORT/nr_ulsch_demodulation.c b/openair1/PHY/NR_TRANSPORT/nr_ulsch_demodulation.c index 75ee35385f..4640ff0746 100644 --- a/openair1/PHY/NR_TRANSPORT/nr_ulsch_demodulation.c +++ b/openair1/PHY/NR_TRANSPORT/nr_ulsch_demodulation.c @@ -1263,7 +1263,7 @@ static void inner_rx(PHY_VARS_gNB *gNB, int nb_layer = rel15_ul->nrOfLayers; int nb_rx_ant = frame_parms->nb_antennas_rx; int dmrs_symbol_flag = (rel15_ul->ul_dmrs_symb_pos >> symbol) & 0x01; - int buffer_length = (rel15_ul->rb_size * NR_NB_SC_PER_RB + 15) & ~15; + int buffer_length = ALIGN_UP_16(rel15_ul->rb_size * NR_NB_SC_PER_RB); c16_t rxFext[nb_rx_ant][buffer_length] __attribute__((aligned(32))); c16_t chFext[nb_layer][nb_rx_ant][buffer_length] __attribute__((aligned(32))); @@ -1631,7 +1631,7 @@ int nr_rx_pusch_tp(PHY_VARS_gNB *gNB, // extract the data in the OFDM frame, to the start of the array int soffset = (slot % RU_RX_SLOT_DEPTH) * frame_parms->symbols_per_slot * frame_parms->ofdm_symbol_size; - nb_re_pusch = (nb_re_pusch + 15) & ~15; + nb_re_pusch = ALIGN_UP_16(nb_re_pusch); int dmrs_symbol; if (gNB->chest_time == 0) dmrs_symbol = get_valid_dmrs_idx_for_channel_est(rel15_ul->ul_dmrs_symb_pos, meas_symbol); @@ -1699,8 +1699,8 @@ int nr_rx_pusch_tp(PHY_VARS_gNB *gNB, start_meas(&gNB->rx_pusch_symbol_processing_stats); int numSymbols = gNB->num_pusch_symbols_per_thread; + int total_res = 0; for(uint8_t symbol = rel15_ul->start_symbol_index; symbol < end_symbol; symbol += numSymbols) { - int total_res = 0; for (int s = 0; s < numSymbols; s++) { pusch_vars->ul_valid_re_per_slot[symbol+s] = get_nb_re_pusch(frame_parms,rel15_ul,symbol+s); pusch_vars->llr_offset[symbol+s] = ((symbol+s) == rel15_ul->start_symbol_index) ? @@ -1744,5 +1744,26 @@ int nr_rx_pusch_tp(PHY_VARS_gNB *gNB, } stop_meas(&gNB->rx_pusch_symbol_processing_stats); + + // Copy the data to the scope. This cannot be performed in one call to gNBscopeCopy because the data is not contiguous in the + // buffer due to reference symbol extraction and padding. The gNBscopeCopy call is broken up into steps: trylock, copy, unlock. + metadata mt = {.slot = slot, .frame = frame}; + if (gNBTryLockScopeData(gNB, gNBPuschRxIq, sizeof(c16_t), 1, total_res, &mt)) { + int buffer_length = ALIGN_UP_16(rel15_ul->rb_size * NR_NB_SC_PER_RB); + size_t offset = 0; + for (uint8_t symbol = rel15_ul->start_symbol_index; symbol < (rel15_ul->start_symbol_index + rel15_ul->nr_of_symbols); + symbol++) { + gNBscopeCopyUnsafe(gNB, + gNBPuschRxIq, + &pusch_vars->rxdataF_comp[0][symbol * buffer_length], + sizeof(c16_t) * pusch_vars->ul_valid_re_per_slot[symbol], + offset, + symbol - rel15_ul->start_symbol_index); + offset += sizeof(c16_t) * pusch_vars->ul_valid_re_per_slot[symbol]; + } + gNBunlockScopeData(gNB, gNBPuschRxIq) + } + uint32_t total_llrs = total_res * rel15_ul->qam_mod_order * rel15_ul->nrOfLayers; + gNBscopeCopyWithMetadata(gNB, gNBPuschLlr, pusch_vars->llr, sizeof(c16_t), 1, total_llrs, 0, &mt); return 0; } diff --git a/openair1/PHY/NR_UE_TRANSPORT/nr_dlsch_demodulation.c b/openair1/PHY/NR_UE_TRANSPORT/nr_dlsch_demodulation.c index 6e17495925..ee83cccad0 100644 --- a/openair1/PHY/NR_UE_TRANSPORT/nr_dlsch_demodulation.c +++ b/openair1/PHY/NR_UE_TRANSPORT/nr_dlsch_demodulation.c @@ -718,6 +718,24 @@ int nr_rx_pdsch(PHY_VARS_NR_UE *ue, } */ #endif + if (UEScopeHasTryLock(ue)) { + metadata mt = {.frame = proc->frame_rx, .slot = proc->nr_slot_rx }; + int total_valid_res = 0; + for (int i = startSymbIdx; i < startSymbIdx + nbSymb; i++) { + total_valid_res = dl_valid_re[i - 1]; + } + if (UETryLockScopeData(ue, pdschRxdataF_comp, sizeof(c16_t), 1, total_valid_res, &mt)) { + size_t offset = 0; + for (int i = startSymbIdx; i < startSymbIdx + nbSymb; i++) { + size_t data_size = sizeof(c16_t) * dl_valid_re[i - i]; + UEscopeCopyUnsafe(ue, pdschRxdataF_comp, &rxdataF_comp[0][0][rx_size_symbol * i], data_size, offset, i - startSymbIdx); + offset += data_size; + } + UEunlockScopeData(ue, pdschRxdataF_comp) + } + } else { + UEscopeCopy(ue, pdschRxdataF_comp, rxdataF_comp[0], sizeof(c16_t), nbRx, rx_size_symbol * NR_SYMBOLS_PER_SLOT, 0); + } } if (meas_enabled) { @@ -741,7 +759,6 @@ int nr_rx_pdsch(PHY_VARS_NR_UE *ue, T_INT(frame_parms->symbols_per_slot), T_BUFFER(&rxdataF_comp[gNB_id][0], 2 * /* ulsch[UE_id]->harq_processes[harq_pid]->nb_rb */ frame_parms->N_RB_UL * 12 * 2)); #endif - UEscopeCopy(ue, pdschRxdataF_comp, rxdataF_comp[0], sizeof(c16_t), nbRx, rx_size_symbol * NR_SYMBOLS_PER_SLOT, 0); if (ue->phy_sim_pdsch_rxdataF_comp) for (int a = 0; a < nbRx; a++) { diff --git a/openair1/PHY/NR_UE_TRANSPORT/nr_pbch.c b/openair1/PHY/NR_UE_TRANSPORT/nr_pbch.c index d09978728a..82819551e4 100644 --- a/openair1/PHY/NR_UE_TRANSPORT/nr_pbch.c +++ b/openair1/PHY/NR_UE_TRANSPORT/nr_pbch.c @@ -442,8 +442,9 @@ int nr_rx_pbch(PHY_VARS_NR_UE *ue, // legacy code use int16, but it is complex16 if (ue) { - UEscopeCopy(ue, pbchRxdataF_comp, pbch_unClipped, sizeof(struct complex16), frame_parms->nb_antennas_rx, pbch_e_rx_idx / 2, 0); - UEscopeCopy(ue, pbchLlr, pbch_e_rx, sizeof(int16_t), frame_parms->nb_antennas_rx, pbch_e_rx_idx, 0); + metadata meta = {.slot = proc->nr_slot_rx, .frame = proc->frame_rx}; + UEscopeCopyWithMetadata(ue, pbchRxdataF_comp, pbch_unClipped, sizeof(struct complex16), frame_parms->nb_antennas_rx, pbch_e_rx_idx / 2, 0, &meta); + UEscopeCopyWithMetadata(ue, pbchLlr, pbch_e_rx, sizeof(int16_t), frame_parms->nb_antennas_rx, pbch_e_rx_idx, 0, &meta); } #ifdef DEBUG_PBCH for (int cnt = 0; cnt < 864 ; cnt++) diff --git a/openair1/PHY/TOOLS/CMakeLists.txt b/openair1/PHY/TOOLS/CMakeLists.txt index 93ef4f5cc7..ada3d8c863 100644 --- a/openair1/PHY/TOOLS/CMakeLists.txt +++ b/openair1/PHY/TOOLS/CMakeLists.txt @@ -63,3 +63,7 @@ endif() if (ENABLE_TESTS) add_subdirectory(tests) endif() + +if (ENABLE_IMSCOPE) + add_subdirectory(imscope) +endif() diff --git a/openair1/PHY/TOOLS/imscope/CMakeLists.txt b/openair1/PHY/TOOLS/imscope/CMakeLists.txt new file mode 100644 index 0000000000..60b2dc69ce --- /dev/null +++ b/openair1/PHY/TOOLS/imscope/CMakeLists.txt @@ -0,0 +1,34 @@ + +CPMAddPackage("gh:ocornut/imgui#v1.90.9") +add_library(imgui + ${imgui_SOURCE_DIR}/imgui_draw.cpp + ${imgui_SOURCE_DIR}/imgui.cpp + ${imgui_SOURCE_DIR}/imgui_widgets.cpp + ${imgui_SOURCE_DIR}/imgui_tables.cpp + ${imgui_SOURCE_DIR}/imgui_demo.cpp +) +target_include_directories(imgui PUBLIC ${imgui_SOURCE_DIR}) + +add_library(imgui_opengl_renderer ${imgui_SOURCE_DIR}/backends/imgui_impl_opengl3.cpp) +target_include_directories(imgui_opengl_renderer PUBLIC ${imgui_SOURCE_DIR}/backends/) +target_link_libraries(imgui_opengl_renderer PUBLIC imgui) + +add_library(imgui_glfw_backend ${imgui_SOURCE_DIR}/backends/imgui_impl_glfw.cpp) +target_include_directories(imgui_glfw_backend PUBLIC ${imgui_SOURCE_DIR}/backends/) +target_link_libraries(imgui_glfw_backend PUBLIC imgui) + +find_package(OpenGL REQUIRED) +find_package(glfw3 3.3 REQUIRED) + +CPMAddPackage("gh:epezent/implot#v0.16") +add_library(implot + ${implot_SOURCE_DIR}/implot.cpp + ${implot_SOURCE_DIR}/implot_demo.cpp + ${implot_SOURCE_DIR}/implot_items.cpp +) +target_link_libraries(implot PUBLIC imgui) +target_include_directories(implot PUBLIC ${implot_SOURCE_DIR}) + +add_library(imscope MODULE imscope.cpp ../phy_scope_interface.c) +target_link_libraries(imscope PUBLIC imgui_glfw_backend glfw imgui_opengl_renderer OpenGL::OpenGL implot UTIL) +set_target_properties(imscope PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) diff --git a/openair1/PHY/TOOLS/imscope/imscope.cpp b/openair1/PHY/TOOLS/imscope/imscope.cpp new file mode 100644 index 0000000000..8205c85b9e --- /dev/null +++ b/openair1/PHY/TOOLS/imscope/imscope.cpp @@ -0,0 +1,847 @@ +/* + * Licensed to the OpenAirInterface (OAI) Software Alliance under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The OpenAirInterface Software Alliance licenses this file to You under + * the OAI Public License, Version 1.1 (the "License"); you may not use this file + * except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.openairinterface.org/?page_id=698 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *------------------------------------------------------------------------------- + * For more information about the OpenAirInterface (OAI) Software Alliance: + * contact@openairinterface.org + */ + +#include "imgui.h" +#include "imgui_impl_glfw.h" +#include "imgui_impl_opengl3.h" +#include +#define GL_SILENCE_DEPRECATION +#if defined(IMGUI_IMPL_OPENGL_ES2) +#include +#endif +#include // Will drag system OpenGL headers + +#include "implot.h" +#include "openair1/PHY/defs_nr_UE.h" +extern "C" { +#include "openair1/PHY/TOOLS/phy_scope_interface.h" +uint64_t get_softmodem_optmask(void); +} +#include +#include +#include +#include +#include +#include +#include +#include "executables/softmodem-bits.h" + +#define MAX_OFFSETS 14 +#define NR_MAX_RB 273 +#define N_SC_PER_RB NR_NB_SC_PER_RB + +static std::vector rb_boundaries; + +void copyDataThreadSafe(void *scopeData, + enum scopeDataType type, + void *dataIn, + int elementSz, + int colSz, + int lineSz, + int offset, + metadata *meta); +bool tryLockScopeData(enum scopeDataType type, int elementSz, int colSz, int lineSz, metadata *meta); +void copyDataUnsafeWithOffset(enum scopeDataType type, void *dataIn, size_t size, size_t offset, int copy_index); +void unlockScopeData(enum scopeDataType type); + +static void glfw_error_callback(int error, const char *description) +{ + fprintf(stderr, "GLFW Error %d: %s\n", error, description); +} + +typedef struct ImScopeData { + std::mutex write_mutex; + scopeGraphData_t *scope_graph_data; + bool is_data_ready; + metadata meta; + uint64_t time_taken_in_ns; + uint64_t time_taken_in_ns_per_offset[MAX_OFFSETS]; + size_t data_copied_per_offset[MAX_OFFSETS]; +} ImScopeData; + +typedef struct MovingAverageTimer { + uint64_t sum = 0; + float average = 0; + float last_update_time = 0; + void UpdateAverage(float time) + { + if (time > last_update_time + 1) { + float new_average = sum / (float)((time - last_update_time) / 1000); + average = 0.95 * average + 0.05 * new_average; + sum = 0; + } + } + void Add(uint64_t ns) + { + sum += ns; + } +} MovingAverageTimer; + +MovingAverageTimer iq_procedure_timer; + +static ImScopeData scope_array[EXTRA_SCOPE_TYPES]; + +typedef struct IQData { + std::vector real; + std::vector imag; + int16_t max_iq; + std::vector power; + float max_power; + float timestamp; + int nonzero_count; + int len; + metadata meta; + + bool TryCollect(ImScopeData *imscope_data, float time, float epsilon) + { + if (imscope_data->is_data_ready) { + iq_procedure_timer.Add(imscope_data->time_taken_in_ns); + timestamp = time; + scopeGraphData_t *iq_header = imscope_data->scope_graph_data; + len = iq_header->lineSz; + real.reserve(len); + imag.reserve(len); + power.reserve(len); + c16_t *source = (c16_t *)(iq_header + 1); + max_iq = 0; + nonzero_count = 0; + for (auto i = 0; i < len; i++) { + real[i] = source[i].r; + imag[i] = source[i].i; + max_iq = std::max(max_iq, (int16_t)std::abs(source[i].r)); + max_iq = std::max(max_iq, (int16_t)std::abs(source[i].i)); + nonzero_count = std::abs(source[i].r) > epsilon || std::abs(source[i].i) > epsilon ? nonzero_count + 1 : nonzero_count; + power[i] = std::sqrt(std::pow(source[i].r, 2) + std::pow(source[i].i, 2)); + } + meta = imscope_data->meta; + imscope_data->is_data_ready = false; + return true; + } + return false; + } +} IQData; + +class LLRPlot { + int len = 0; + float timestamp = 0; + std::vector llr; + bool frozen = false; + bool next = false; + metadata meta; + + public: + void Draw(float time, enum scopeDataType type, const char *label) + { + ImGui::BeginGroup(); + if (ImGui::Button(frozen ? "Unfreeze" : "Freeze")) { + frozen = !frozen; + next = false; + } + if (frozen) { + ImGui::SameLine(); + ImGui::BeginDisabled(next); + if (ImGui::Button("Load next histogram")) { + next = true; + } + ImGui::EndDisabled(); + } + + ImScopeData &scope_data = scope_array[type]; + if (ImPlot::BeginPlot(label)) { + if (!frozen || next) { + if (scope_data.is_data_ready) { + iq_procedure_timer.Add(scope_data.time_taken_in_ns); + timestamp = time; + const int16_t *tmp = (int16_t *)(scope_data.scope_graph_data + 1); + len = scope_data.scope_graph_data->lineSz; + llr.reserve(len); + for (auto i = 0; i < len; i++) { + llr[i] = tmp[i]; + } + meta = scope_data.meta; + scope_data.is_data_ready = false; + if (frozen) { + next = false; + } + } + } + + ImPlot::PlotLine(label, llr.data(), len); + ImPlot::EndPlot(); + } + std::stringstream ss; + if (meta.slot != -1) { + ss << " slot: " << meta.slot; + } + if (meta.frame != -1) { + ss << " frame: " << meta.frame; + } + if (!ss.str().empty()) { + ImGui::Text("Data for %s", ss.str().c_str()); + } + ImGui::Text("Data is %.2f seconds old", time - timestamp); + ImGui::EndGroup(); + } +}; + +class IQHist { + private: + bool frozen = false; + bool next = false; + float range = 100; + int num_bins = 33; + std::string label; + float min_nonzero_percentage = 0.9; + float epsilon = 0.0; + bool auto_adjust_range = true; + int plot_type = 0; + + public: + IQHist(const char *label_) + { + label = label_; + }; + bool ShouldReadData(void) + { + return !frozen || next; + } + float GetEpsilon(void) + { + return epsilon; + } + void Draw(IQData *iq_data, float time, bool new_data) + { + if (new_data && frozen && next) { + // Evaluate if new data matches filter settings + if (((float)iq_data->nonzero_count / (float)iq_data->len) > min_nonzero_percentage) { + next = false; + } + } + ImGui::BeginGroup(); + ImGui::Checkbox("auto adjust range", &auto_adjust_range); + if (auto_adjust_range) { + if (range < iq_data->max_iq * 1.1) { + range = iq_data->max_iq * 1.1; + } + } + ImGui::BeginDisabled(auto_adjust_range); + ImGui::SameLine(); + ImGui::DragFloat("Range", &range, 1, 0.1, std::numeric_limits::max()); + ImGui::EndDisabled(); + + ImGui::DragInt("Number of bins", &num_bins, 1, 33, 101); + if (ImGui::Button(frozen ? "Unfreeze" : "Freeze")) { + frozen = !frozen; + next = false; + } + + if (frozen) { + ImGui::SameLine(); + ImGui::BeginDisabled(next); + if (ImGui::Button("Load next histogram")) { + next = true; + } + ImGui::EndDisabled(); + ImGui::Text("Filter parameters"); + ImGui::DragFloat("%% nonzero elements", &min_nonzero_percentage, 1, 0.0, 100); + ImGui::DragFloat("epsilon", &epsilon, 1, 0.0, 3000); + } + const char *items[] = {"Histogram", "Scatter", "RMS"}; + ImGui::Combo("Select plot type", &plot_type, items, sizeof(items) / sizeof(items[0])); + if (plot_type == 0) { + if (ImPlot::BeginPlot(label.c_str(), {(float)ImGui::GetWindowWidth() * 0.3f, (float)ImGui::GetWindowWidth() * 0.3f})) { + ImPlot::PlotHistogram2D(label.c_str(), + iq_data->real.data(), + iq_data->imag.data(), + iq_data->len, + num_bins, + num_bins, + ImPlotRect(-range, range, -range, range)); + ImPlot::EndPlot(); + } + } else if (plot_type == 1) { + if (ImPlot::BeginPlot(label.c_str(), {(float)ImGui::GetWindowWidth() * 0.3f, (float)ImGui::GetWindowWidth() * 0.3f})) { + // Limit the amount of data plotted with scatterplot (issue with vertices/draw call) + ImPlot::PlotScatter(label.c_str(), iq_data->real.data(), iq_data->imag.data(), std::min(16000, iq_data->len)); + ImPlot::EndPlot(); + } + } else if (plot_type == 2) { + if (ImPlot::BeginPlot(label.c_str())) { + ImPlot::PlotLine(label.c_str(), iq_data->power.data(), iq_data->len); + ImPlot::EndPlot(); + } + } + ImGui::Text("Maximum value = %d, nonzero elements/total %d/%d", iq_data->max_iq, iq_data->nonzero_count, iq_data->len); + ImGui::Text("Data is %.2f seconds old", time - iq_data->timestamp); + std::stringstream ss; + if (iq_data->meta.slot != -1) { + ss << " slot: " << iq_data->meta.slot; + } + if (iq_data->meta.frame != -1) { + ss << " frame: " << iq_data->meta.frame; + } + if (!ss.str().empty()) { + ImGui::Text("Data for %s", ss.str().c_str()); + } + ImGui::EndGroup(); + } +}; + +class IQSlotHeatmap { + private: + bool frozen = false; + bool next = false; + float timestamp = 0; + std::vector power; + ImScopeData *scope_data; + std::string label; + int len = 0; + float max = 0; + float stop_at_min = 1000; + + public: + IQSlotHeatmap(ImScopeData *scope_data_, const char *label_) + { + scope_data = scope_data_; + label = label_; + }; + // Read in the data from the sink and transform it for the use by the scope + void ReadData(float time, int ofdm_symbol_size, int num_symbols, int first_carrier_offset, int num_rb) + { + auto num_sc = num_rb * NR_NB_SC_PER_RB; + if (!frozen || next) { + if (scope_data->is_data_ready) { + iq_procedure_timer.Add(scope_data->time_taken_in_ns); + uint16_t first_sc = first_carrier_offset; + uint16_t last_sc = first_sc + num_rb * NR_NB_SC_PER_RB; + bool wrapped = false; + uint16_t wrapped_first_sc = 0; + uint16_t wrapped_last_sc = 0; + if (last_sc >= ofdm_symbol_size) { + last_sc = ofdm_symbol_size - 1; + wrapped = true; + auto num_sc_left = num_sc - (last_sc - first_sc + 1); + wrapped_last_sc = wrapped_first_sc + num_sc_left - 1; + } + timestamp = time; + scopeGraphData_t *iq_header = scope_data->scope_graph_data; + len = iq_header->lineSz; + c16_t *source = (c16_t *)(iq_header + 1); + + power.reserve(num_sc * num_symbols); + for (auto symbol = 0; symbol < num_symbols; symbol++) { + int subcarrier = 0; + for (auto sc = first_sc; sc <= last_sc; sc++) { + auto source_index = sc + symbol * ofdm_symbol_size; + power[subcarrier * num_symbols + symbol] = std::pow(source[source_index].r, 2) + std::pow(source[source_index].i, 2); + subcarrier++; + } + if (wrapped) { + for (auto sc = wrapped_first_sc; sc <= wrapped_last_sc; sc++) { + auto source_index = sc + symbol * ofdm_symbol_size; + power[subcarrier * num_symbols + symbol] = std::pow(source[source_index].r, 2) + std::pow(source[source_index].i, 2); + subcarrier++; + } + } + } + max = *std::max_element(power.begin(), power.end()); + if (frozen && max > stop_at_min) { + next = false; + } + scope_data->is_data_ready = false; + } + } + } + void Draw(float time, int ofdm_symbol_size, int num_symbols, int first_carrier_offset, int num_rb) + { + ReadData(time, ofdm_symbol_size, num_symbols, first_carrier_offset, num_rb); + ImGui::BeginGroup(); + if (ImGui::Button(frozen ? "Unfreeze" : "Freeze")) { + frozen = !frozen; + next = false; + } + if (frozen) { + ImGui::SameLine(); + ImGui::BeginDisabled(next); + if (ImGui::Button("Load next data")) { + next = true; + } + ImGui::EndDisabled(); + ImGui::Text("Filter parameters:"); + ImGui::InputFloat("Max Power minimum", &stop_at_min, 10, 100); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Data with maximum power below that value will be discarded."); + } + } + static std::vector symbol_boundaries = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13}; + if (ImPlot::BeginPlot(label.c_str(), {(float)ImGui::GetWindowWidth() * 0.9f, 0})) { + auto num_sc = num_rb * NR_NB_SC_PER_RB; + ImPlot::SetupAxes("symbols", "subcarriers"); + ImPlot::SetupAxisLimits(ImAxis_X1, num_symbols, 0); + ImPlot::SetupAxisLimits(ImAxis_Y1, num_sc, 0); + ImPlot::PlotHeatmap(label.c_str(), + power.data(), + num_sc, + num_symbols, + 0, + max, + nullptr, + {0, 0}, + {(double)num_symbols, (double)num_sc}); + ImPlot::PlotInfLines("Symbol boundary", symbol_boundaries.data(), symbol_boundaries.size()); + ImPlot::PlotInfLines("RB boundary", rb_boundaries.data(), num_rb, ImPlotInfLinesFlags_Horizontal); + ImPlot::EndPlot(); + } + ImGui::SameLine(); + ImPlot::ColormapScale("##HeatScale", 0, max); + ImGui::Text("Data is %.2f seconds old", time - timestamp); + ImGui::EndGroup(); + } +}; + +// utility structure for realtime plot +struct ScrollingBuffer { + int MaxSize; + int Offset; + ImVector Data; + ScrollingBuffer(int max_size = 2000) + { + MaxSize = max_size; + Offset = 0; + Data.reserve(MaxSize); + } + void AddPoint(float x, float y) + { + if (Data.size() < MaxSize) + Data.push_back(ImVec2(x, y)); + else { + Data[Offset] = ImVec2(x, y); + Offset = (Offset + 1) % MaxSize; + } + } + void Erase() + { + if (Data.size() > 0) { + Data.shrink(0); + Offset = 0; + } + } +}; + +void ShowUeScope(PHY_VARS_NR_UE *ue, float t) +{ + if (ImPlot::BeginPlot("##Scrolling", ImVec2(-1, 150))) { + static float history = 10.0f; + ImGui::SliderFloat("History", &history, 1, 30, "%.1f s"); + static ScrollingBuffer rbs_buffer; + static ScrollingBuffer bler; + static ScrollingBuffer mcs; + rbs_buffer.AddPoint(t, getKPIUE()->nofRBs); + bler.AddPoint(t, (float)getKPIUE()->nb_nack / (float)getKPIUE()->nb_total); + mcs.AddPoint(t, (float)getKPIUE()->dl_mcs); + ImPlot::SetupAxes("time", "noOfRbs"); + ImPlot::SetupAxisLimits(ImAxis_X1, t - history, t, ImGuiCond_Always); + ImPlot::SetupAxisLimits(ImAxis_Y1, 0, NR_MAX_RB); + ImPlot::SetupAxis(ImAxis_Y2, "bler [%]", ImPlotAxisFlags_AuxDefault); + ImPlot::SetupAxis(ImAxis_Y3, "MCS", ImPlotAxisFlags_AuxDefault); + ImPlot::SetAxes(ImAxis_X1, ImAxis_Y1); + ImPlot::PlotLine("noOfRbs", &rbs_buffer.Data[0].x, &rbs_buffer.Data[0].y, rbs_buffer.Data.size(), 0, 0, 2 * sizeof(float)); + ImPlot::SetAxes(ImAxis_X1, ImAxis_Y2); + ImPlot::PlotLine("bler", &bler.Data[0].x, &bler.Data[0].y, bler.Data.size(), 0, 0, 2 * sizeof(float)); + ImPlot::SetAxes(ImAxis_X1, ImAxis_Y3); + ImPlot::PlotLine("mcs", &mcs.Data[0].x, &mcs.Data[0].y, mcs.Data.size(), 0, 0, 2 * sizeof(float)); + ImPlot::EndPlot(); + } + if (ImGui::TreeNode("PDSCH IQ")) { + static auto iq_data = new IQData(); + static auto pdsch_iq_hist = new IQHist("PDSCH IQ"); + bool new_data = false; + if (pdsch_iq_hist->ShouldReadData()) { + new_data = iq_data->TryCollect(&scope_array[pdschRxdataF_comp], t, pdsch_iq_hist->GetEpsilon()); + } + pdsch_iq_hist->Draw(iq_data, t, new_data); + ImGui::TreePop(); + } + if (ImGui::TreeNode("Broadcast channel")) { + ImGui::Text("RSRP %d", ue->measurements.ssb_rsrp_dBm[ue->frame_parms.ssb_index]); + if (ImGui::TreeNode("IQ")) { + static auto iq_data = new IQData(); + static auto broadcast_iq_hist = new IQHist("Broadcast IQ"); + bool new_data = false; + if (broadcast_iq_hist->ShouldReadData()) { + new_data = iq_data->TryCollect(&scope_array[ue->sl_mode ? psbchRxdataF_comp : pbchRxdataF_comp], + t, + broadcast_iq_hist->GetEpsilon()); + } + broadcast_iq_hist->Draw(iq_data, t, new_data); + ImGui::TreePop(); + } + if (ImGui::TreeNode("CHest")) { + static auto chest_iq_data = new IQData(); + static auto broadcast_iq_chest = new IQHist("Broadcast Chest"); + bool new_data = false; + if (broadcast_iq_chest->ShouldReadData()) { + new_data = chest_iq_data->TryCollect(&scope_array[ue->sl_mode ? psbchDlChEstimateTime : pbchDlChEstimateTime], + t, + broadcast_iq_chest->GetEpsilon()); + } + broadcast_iq_chest->Draw(chest_iq_data, t, new_data); + ImGui::TreePop(); + } + + if (ImGui::TreeNode("LLR")) { + static auto llr_plot = new LLRPlot(); + llr_plot->Draw(t, ue->sl_mode ? psbchLlr : pbchLlr, "Broadcast LLR"); + ImGui::TreePop(); + } + ImGui::TreePop(); + } + if (ImGui::TreeNode("RX IQ")) { + static auto common_rx_iq_heatmap = new IQSlotHeatmap(&scope_array[commonRxdataF], "common RX IQ"); + common_rx_iq_heatmap->Draw(t, + ue->frame_parms.ofdm_symbol_size, + ue->frame_parms.symbols_per_slot, + ue->frame_parms.first_carrier_offset, + ue->frame_parms.N_RB_DL); + ImGui::TreePop(); + } +} + +void ShowGnbScope(PHY_VARS_gNB *gNB, float t) +{ + if (ImGui::TreeNode("RX IQ")) { + static auto gnb_heatmap = new IQSlotHeatmap(&scope_array[gNBRxdataF], "common RX IQ"); + + gnb_heatmap->Draw(t, + gNB->frame_parms.ofdm_symbol_size, + gNB->frame_parms.symbols_per_slot, + gNB->frame_parms.first_carrier_offset, + gNB->frame_parms.N_RB_UL); + ImGui::TreePop(); + } + if (ImGui::TreeNode("PUSCH SLOT IQ")) { + static auto pusch_iq = new IQData(); + static auto pusch_iq_display = new IQHist("PUSCH compensated IQ"); + bool new_data = false; + if (pusch_iq_display->ShouldReadData()) { + new_data = pusch_iq->TryCollect(&scope_array[gNBPuschRxIq], t, pusch_iq_display->GetEpsilon()); + } + pusch_iq_display->Draw(pusch_iq, t, new_data); + ImGui::TreePop(); + } + if (ImGui::TreeNode("PUSCH LLRs")) { + static auto pusch_llr_plot = new LLRPlot(); + pusch_llr_plot->Draw(t, gNBPuschLlr, "PUSCH LLR"); + ImGui::TreePop(); + } +} + +void *imscope_thread(void *data_void_ptr) +{ + glfwSetErrorCallback(glfw_error_callback); + if (!glfwInit()) + return nullptr; + + // Decide GL+GLSL versions +#if defined(IMGUI_IMPL_OPENGL_ES2) + // GL ES 2.0 + GLSL 100 + const char *glsl_version = "#version 100"; + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); + glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API); +#elif defined(__APPLE__) + // GL 3.2 + GLSL 150 + const char *glsl_version = "#version 150"; + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 3.2+ only + glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // Required on Mac +#else + // GL 3.0 + GLSL 130 + const char *glsl_version = "#version 130"; + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); + // glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 3.2+ only + // glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // 3.0+ only +#endif + + // Create window with graphics context + GLFWwindow *window = glfwCreateWindow(1280, 720, "imscope", nullptr, nullptr); + if (window == nullptr) + return nullptr; + glfwMakeContextCurrent(window); + glfwSwapInterval(1); // For frame capping + + // Setup Dear ImGui context + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImPlot::CreateContext(); + ImGuiIO &io = ImGui::GetIO(); + (void)io; + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls + + // Setup Dear ImGui style + ImGui::StyleColorsDark(); + // ImGui::StyleColorsLight(); + + // Setup Platform/Renderer backends + ImGui_ImplGlfw_InitForOpenGL(window, true); +#ifdef __EMSCRIPTEN__ + ImGui_ImplGlfw_InstallEmscriptenCallbacks(window, "#canvas"); +#endif + ImGui_ImplOpenGL3_Init(glsl_version); + + // Our state + ImVec4 clear_color = ImVec4(0.45f, 0.55f, 0.60f, 1.00f); + + for (auto i = 0U; i < NR_MAX_RB; i++) { + rb_boundaries.push_back(i * NR_NB_SC_PER_RB); + } + + static double last_frame_time = glfwGetTime(); + static int target_fps = 24; + + bool is_ue = (get_softmodem_optmask() & SOFTMODEM_5GUE_BIT) > 0; + while (!glfwWindowShouldClose(window)) { + // Poll and handle events (inputs, window resize, etc.) + // You can read the io.WantCaptureMouse, io.WantCaptureKeyboard flags to tell if dear imgui wants to use your inputs. + // - When io.WantCaptureMouse is true, do not dispatch mouse input data to your main application, or clear/overwrite your copy + // of the mouse data. + // - When io.WantCaptureKeyboard is true, do not dispatch keyboard input data to your main application, or clear/overwrite your + // copy of the keyboard data. Generally you may always pass all inputs to dear imgui, and hide them from your application based + // on those two flags. + glfwPollEvents(); + + // Start the Dear ImGui frame + ImGui_ImplOpenGL3_NewFrame(); + ImGui_ImplGlfw_NewFrame(); + ImGui::NewFrame(); + + int display_w, display_h; + glfwGetFramebufferSize(window, &display_w, &display_h); + + static float t = 0; + + t += ImGui::GetIO().DeltaTime; + ImGui::SetNextWindowPos({0, 0}); + ImGui::SetNextWindowSize({(float)display_w, (float)display_h}); + ImGui::Begin("NR KPI", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove); + if (ImGui::TreeNode("Global settings")) { + ImGui::ShowFontSelector("Font"); + ImGui::ShowStyleSelector("ImGui Style"); + ImPlot::ShowStyleSelector("ImPlot Style"); + ImPlot::ShowColormapSelector("ImPlot Colormap"); + ImGui::SliderInt("FPS target", &target_fps, 12, 60); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Reduces scope flickering in unfrozen mode. Can reduce impact on perfromance of the modem"); + } + ImGui::TreePop(); + } + iq_procedure_timer.UpdateAverage(t); + ImGui::Text("Total time used by IQ capture procedures per milisecond: %.2f [us]/[ms]", iq_procedure_timer.average / 1000); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Total time used in PHY threads for copying out IQ data for the scope, in uS, averaged over 1 ms"); + } + + if (is_ue) { + PHY_VARS_NR_UE *ue = (PHY_VARS_NR_UE *)data_void_ptr; + ShowUeScope(ue, t); + } else { + scopeParms_t *scope_params = (scopeParms_t *)data_void_ptr; + PHY_VARS_gNB *gNB = scope_params->gNB; + ShowGnbScope(gNB, t); + } + ImGui::End(); + + // For reference + ImPlot::ShowDemoWindow(); + ImGui::ShowDemoWindow(); + + // Rendering + ImGui::Render(); + glViewport(0, 0, display_w, display_h); + glClearColor(clear_color.x * clear_color.w, clear_color.y * clear_color.w, clear_color.z * clear_color.w, clear_color.w); + glClear(GL_COLOR_BUFFER_BIT); + ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + + double target_frame_time = 1.0 / target_fps; + double delta = glfwGetTime() - last_frame_time; + if (delta < target_frame_time) { + std::this_thread::sleep_for(std::chrono::duration(target_frame_time - delta)); + } + + glfwSwapBuffers(window); + last_frame_time = glfwGetTime(); + } + + // Cleanup + ImGui_ImplOpenGL3_Shutdown(); + ImGui_ImplGlfw_Shutdown(); + ImGui::DestroyContext(); + + glfwDestroyWindow(window); + glfwTerminate(); + + return nullptr; +} + +extern "C" void imscope_autoinit(void *dataptr) +{ + AssertFatal((get_softmodem_optmask() & SOFTMODEM_5GUE_BIT) || (get_softmodem_optmask() & SOFTMODEM_GNB_BIT), + "Scope cannot find NRUE or GNB context"); + + for (auto i = 0U; i < EXTRA_SCOPE_TYPES; i++) { + scope_array[i].is_data_ready = false; + scope_array[i].scope_graph_data = nullptr; + scope_array[i].meta = {-1, -1}; + } + + if (SOFTMODEM_GNB_BIT & get_softmodem_optmask()) { + scopeParms_t *scope_params = (scopeParms_t *)dataptr; + scopeData_t *scope = (scopeData_t *)calloc(1, sizeof(scopeData_t)); + scope->copyData = copyDataThreadSafe; + scope->tryLockScopeData = tryLockScopeData; + scope->copyDataUnsafeWithOffset = copyDataUnsafeWithOffset; + scope->unlockScopeData = unlockScopeData; + scope_params->gNB->scopeData = scope; + } else { + PHY_VARS_NR_UE *ue = (PHY_VARS_NR_UE *)dataptr; + scopeData_t *scope = (scopeData_t *)calloc(1, sizeof(scopeData_t)); + scope->copyData = copyDataThreadSafe; + ue->scopeData = scope; + } + pthread_t thread; + threadCreate(&thread, imscope_thread, dataptr, (char *)"imscope", -1, sched_get_priority_min(SCHED_RR)); +} + +void copyDataThreadSafe(void *scopeData, + enum scopeDataType type, + void *dataIn, + int elementSz, + int colSz, + int lineSz, + int offset, + metadata *meta) +{ + ImScopeData &scope_data = scope_array[type]; + + if (scope_data.is_data_ready) { + // data is ready, wasn't consumed yet by scope + return; + } + + if (scope_data.write_mutex.try_lock()) { + auto start = std::chrono::high_resolution_clock::now(); + scopeGraphData_t *data = scope_data.scope_graph_data; + int oldDataSz = data ? data->dataSize : 0; + int newSz = elementSz * colSz * lineSz; + if (data == NULL || oldDataSz < newSz) { + free(data); + scopeGraphData_t *ptr = (scopeGraphData_t *)malloc(sizeof(scopeGraphData_t) + newSz); + if (!ptr) { + LOG_E(PHY, "can't realloc\n"); + return; + } else { + data = ptr; + } + } + + data->elementSz = elementSz; + data->colSz = colSz; + data->lineSz = lineSz; + data->dataSize = newSz; + memcpy(((void *)(data + 1)), dataIn, newSz); + scope_data.scope_graph_data = data; + scope_data.meta = *meta; + scope_data.time_taken_in_ns = + std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start).count(); + scope_data.is_data_ready = true; + scope_data.write_mutex.unlock(); + } +} + +bool tryLockScopeData(enum scopeDataType type, int elementSz, int colSz, int lineSz, metadata *meta) +{ + ImScopeData &scope_data = scope_array[type]; + + if (scope_data.is_data_ready) { + // data is ready, wasn't consumed yet by scope + return false; + } + + if (scope_data.write_mutex.try_lock()) { + auto start = std::chrono::high_resolution_clock::now(); + scopeGraphData_t *data = scope_data.scope_graph_data; + int oldDataSz = data ? data->dataSize : 0; + int newSz = elementSz * colSz * lineSz; + if (data == NULL || oldDataSz < newSz) { + free(data); + scopeGraphData_t *ptr = (scopeGraphData_t *)malloc(sizeof(scopeGraphData_t) + newSz); + if (!ptr) { + LOG_E(PHY, "can't realloc\n"); + return false; + } else { + data = ptr; + } + } + + data->elementSz = elementSz; + data->colSz = colSz; + data->lineSz = lineSz; + data->dataSize = newSz; + scope_data.scope_graph_data = data; + scope_data.meta = *meta; + scope_data.time_taken_in_ns = + std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start).count(); + memset(scope_data.time_taken_in_ns_per_offset, 0, sizeof(scope_data.time_taken_in_ns_per_offset)); + memset(scope_data.data_copied_per_offset, 0, sizeof(scope_data.data_copied_per_offset)); + return true; + } + return false; +} + +void copyDataUnsafeWithOffset(enum scopeDataType type, void *dataIn, size_t size, size_t offset, int copy_index) +{ + AssertFatal(copy_index < MAX_OFFSETS, "Unexpected number of copies per sink. copy_index = %d\n", copy_index); + ImScopeData &scope_data = scope_array[type]; + auto start = std::chrono::high_resolution_clock::now(); + scopeGraphData_t *data = scope_data.scope_graph_data; + uint8_t *outptr = (uint8_t *)(data + 1); + memcpy(&outptr[offset], dataIn, size); + scope_data.time_taken_in_ns_per_offset[copy_index] = + std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start).count(); + scope_data.data_copied_per_offset[copy_index] = size; +} + +void unlockScopeData(enum scopeDataType type) +{ + ImScopeData &scope_data = scope_array[type]; + size_t total_size = 0; + for (auto i = 0; i < MAX_OFFSETS; i++) { + scope_data.time_taken_in_ns += scope_data.time_taken_in_ns_per_offset[i]; + total_size += scope_data.data_copied_per_offset[i]; + } + if (total_size != (uint64_t)scope_data.scope_graph_data->dataSize) { + LOG_E(PHY, "Scope is missing data - not all data that was expected was copied - possibly missed copyDataUnsafeWithOffset call\n"); + } + scope_data.is_data_ready = true; + scope_data.write_mutex.unlock(); +} diff --git a/openair1/PHY/TOOLS/imscope/imscope_screenshot.png b/openair1/PHY/TOOLS/imscope/imscope_screenshot.png new file mode 100644 index 0000000000..e33b294827 Binary files /dev/null and b/openair1/PHY/TOOLS/imscope/imscope_screenshot.png differ diff --git a/openair1/PHY/TOOLS/phy_scope_interface.c b/openair1/PHY/TOOLS/phy_scope_interface.c index 14f361362d..70d3889b64 100644 --- a/openair1/PHY/TOOLS/phy_scope_interface.c +++ b/openair1/PHY/TOOLS/phy_scope_interface.c @@ -58,8 +58,11 @@ int end_forms(void) { return -1; } -void copyData(void *scopeData, enum scopeDataType type, void *dataIn, int elementSz, int colSz, int lineSz, int offset) +void copyData(void *scopeData, enum scopeDataType type, void *dataIn, int elementSz, int colSz, int lineSz, int offset, metadata *meta) { + if (type >= MAX_SCOPE_TYPES) { + return; + } scopeData_t *tmp = (scopeData_t *)scopeData; if (tmp) { diff --git a/openair1/PHY/TOOLS/phy_scope_interface.h b/openair1/PHY/TOOLS/phy_scope_interface.h index 7c5e33b379..2f8378e7b7 100644 --- a/openair1/PHY/TOOLS/phy_scope_interface.h +++ b/openair1/PHY/TOOLS/phy_scope_interface.h @@ -70,7 +70,10 @@ enum scopeDataType { psbchDlChEstimateTime, psbchLlr, psbchRxdataF_comp, - MAX_SCOPE_TYPES + MAX_SCOPE_TYPES, + gNBPuschRxIq = MAX_SCOPE_TYPES, + gNBPuschLlr, + EXTRA_SCOPE_TYPES }; enum PlotTypeGnbIf { @@ -87,34 +90,89 @@ typedef struct { int lineSz; } scopeGraphData_t; +typedef struct { + int slot; + int frame; +} metadata; + typedef struct scopeData_s { int *argc; char **argv; RU_t *ru; PHY_VARS_gNB *gNB; scopeGraphData_t *liveData[MAX_SCOPE_TYPES]; - void (*copyData)(void *, enum scopeDataType, void *data, int elementSz, int colSz, int lineSz, int offset); + void (*copyData)(void *, enum scopeDataType, void *data, int elementSz, int colSz, int lineSz, int offset, metadata *meta); pthread_mutex_t copyDataMutex; scopeGraphData_t *copyDataBufs[MAX_SCOPE_TYPES][COPIES_MEM]; int copyDataBufsIdx[MAX_SCOPE_TYPES]; void (*scopeUpdater)(enum PlotTypeGnbIf plotType, int numElements); + bool (*tryLockScopeData)(enum scopeDataType type, int elementSz, int colSz, int lineSz, metadata *meta); + void (*copyDataUnsafeWithOffset)(enum scopeDataType type, void *dataIn, size_t size, size_t offset, int copy_index); + void (*unlockScopeData)(enum scopeDataType type); } scopeData_t; int load_softscope(char *exectype, void *initarg); int end_forms(void) ; int copyDataMutexInit(scopeData_t *); -void copyData(void *, enum scopeDataType type, void *dataIn, int elementSz, int colSz, int lineSz, int offset); +void copyData(void *, enum scopeDataType type, void *dataIn, int elementSz, int colSz, int lineSz, int offset, metadata *meta); +#define UEscopeCopyWithMetadata(ue, type, ...) \ + if (ue->scopeData) { \ + ((scopeData_t *)ue->scopeData)->copyData((scopeData_t *)ue->scopeData, type, ##__VA_ARGS__); \ + } #define UEscopeCopy(ue, type, ...) \ - if (ue->scopeData) \ - ((scopeData_t *)ue->scopeData)->copyData((scopeData_t *)ue->scopeData, type, ##__VA_ARGS__); + if (ue->scopeData) { \ + metadata mt = {.slot = -1, .frame = -1}; \ + ((scopeData_t *)ue->scopeData)->copyData((scopeData_t *)ue->scopeData, type, ##__VA_ARGS__, &mt); \ + } +#define gNBscopeCopyWithMetadata(gnb, type, ...) \ + if (gnb->scopeData) { \ + ((scopeData_t *)gnb->scopeData)->copyData((scopeData_t *)gNB->scopeData, type, ##__VA_ARGS__); \ + } #define gNBscopeCopy(gnb, type, ...) \ - if (gnb->scopeData) \ - ((scopeData_t *)gnb->scopeData)->copyData((scopeData_t *)gNB->scopeData, type, ##__VA_ARGS__); + if (gnb->scopeData) { \ + metadata mt = {.slot = -1, .frame = -1}; \ + ((scopeData_t *)gnb->scopeData)->copyData((scopeData_t *)gNB->scopeData, type, ##__VA_ARGS__, &mt); \ + } #define GnbScopeUpdate(gnb, type, numElt) \ if (gnb->scopeData) \ ((scopeData_t *)gnb->scopeData)->scopeUpdater(type, numElt); +#define gNBTryLockScopeData(gnb, type, ...) \ + gnb->scopeData && ((scopeData_t *)gnb->scopeData)->tryLockScopeData \ + && ((scopeData_t *)gnb->scopeData)->tryLockScopeData(type, ##__VA_ARGS__) + +#define gNBscopeCopyUnsafe(gnb, type, ...) \ + scopeData_t *scope_data = (scopeData_t *)gnb->scopeData; \ + if (scope_data && scope_data->copyDataUnsafeWithOffset) { \ + scope_data->copyDataUnsafeWithOffset(type, ##__VA_ARGS__); \ + } + +#define gNBunlockScopeData(gnb, type) \ + scopeData_t *scope_data = (scopeData_t *)gnb->scopeData; \ + if (scope_data && scope_data->unlockScopeData) { \ + scope_data->unlockScopeData(type); \ + } + +#define UEScopeHasTryLock(ue) \ + (ue->scopeData && ((scopeData_t *)ue->scopeData)->tryLockScopeData) + +#define UETryLockScopeData(ue, type, ...) \ + ue->scopeData && ((scopeData_t *)ue->scopeData)->tryLockScopeData \ + && ((scopeData_t *)ue->scopeData)->tryLockScopeData(type, ##__VA_ARGS__) + +#define UEscopeCopyUnsafe(ue, type, ...) \ + scopeData_t *scope_data = (scopeData_t *)ue->scopeData; \ + if (scope_data && scope_data->copyDataUnsafeWithOffset) { \ + scope_data->copyDataUnsafeWithOffset(type, ##__VA_ARGS__); \ + } + +#define UEunlockScopeData(ue, type) \ + scopeData_t *scope_data = (scopeData_t *)ue->scopeData; \ + if (scope_data && scope_data->unlockScopeData) { \ + scope_data->unlockScopeData(type); \ + } + extended_kpi_ue* getKPIUE(); #endif diff --git a/openair1/PHY/TOOLS/readme.md b/openair1/PHY/TOOLS/readme.md index 9a0e2947f9..9d1f4d465b 100644 --- a/openair1/PHY/TOOLS/readme.md +++ b/openair1/PHY/TOOLS/readme.md @@ -31,3 +31,41 @@ or ``` phy_scope_gNB(0, phy_vars_gnb, phy_vars_ru, UE_id) ``` + +# ImScope + +ImScope is a scope based on imgui & implot. This scope uses a different concurrency model than xforms scope, with thread +safety being priority. The goal is to never show incorrect data on the screen and be able to use the scope with real radios. +If correctness cannot be achieved e.g. due to performance issues when using thread safe implementation user should be warned +clearly on the screen. + +![image](./imscope/imscope_screenshot.png) + +## Prerequisites + +ImScope uses imgui, implot, glfw3 and opengl. imgui and implot should be downloaded automatically when configuring the project +with `-DENABLE_IMSCOPE=ON`, using [CPM](https://github.com/cpm-cmake/CPM.cmake). CPM is used because imgui and implot do not have +an official binary release. glfw3 and opengl should be installed with your system, on ubuntu these are contained in packages +libglfw3-dev and libopengl-dev respectivly. + +## Building + +Add `-DENABLE_IMSCOPE=ON` to your `cmake` command. Build target `imscope` + +## Running + +Run with `--imscope` flag + +## Usage notes + + - It's experimental and might contain bugs + - It uses tree nodes to hide/show scopes (layout is subject to change). If a scope is not visible or is frozen it costs nearly + nothing in the PHY threads. If its unfrozen every time the data is displayed the PHY thread would have to perform the copy. By + default this can happen up to 24 times per second. User is informed on the estimated impact on PHY threads at the top of the + window. You can use FPS target to limit the impact on PHY threads and maintain scope functionality while minimizing the effect + on realtime operation. + +## Reporting bugs and feature requests + +Report bugs and feature requests on [gitlab](https://gitlab.eurecom.fr/oai/openairinterface5g/-/issues). There is two demo windows +enabled in the scope that showcase imgui/implot, if you find something of interest it can be implemented in the scope.