Commit be6321c8 authored by Robert Schmidt's avatar Robert Schmidt

Merge remote-tracking branch 'origin/imscope-recorder' into integration_2025_w05 (!3185)

Add IQ file recording and IQ file viewer to ImScope

This changeset introduces the following changes to ImScope:

- Added the ability to record IQ data using the same mechanism as
  ImScope.
- Added a new executable target imscope_iq_file_viewer for viewing IQ
  files.
- Introduced a command line argument --imscope-record that enables data
  recording mode.
- Refactored a lot of the scope code

Usage:

- To record IQ data, run a modem with --imscope-record flag.  The
  recording happens whenever a modem calls dumpScopeData, which
  currently is done only in gNB on PUSCH DTX and PUSCH CRC NOK.  There
  is a limit of ~1GB of files written by the thread in a signle run to
  avoid accidentally exhausting disk space.  If a directory imscope-dump
  is available the data will be recorded there in .imscope files.
  Otherwise files are written to the directory the modem was run. This
  is done so that the directory can be mapped in a docker container.
- To view recorded IQ files, use the new executable
  imscope_iq_file_viewer. This can be done by running:

  ./imscope_iq_file_viewer <path_to_iq_file>

  Replace <path_to_iq_file> with the path to the IQ file you want to view.
parents b34708fa 2ea6ac26
......@@ -298,6 +298,7 @@ message(STATUS "Selected KPM Version: ${KPM_VERSION}")
add_boolean_option(ENABLE_IMSCOPE OFF "Enable phy scope based on imgui" OFF)
add_boolean_option(ENABLE_IMSCOPE_RECORD OFF "Enable recording IQ data for imscope" OFF)
##################################################
# ASN.1 grammar C code generation & dependencies #
......
......@@ -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 ldpc_xdma websrv oai_iqplayer imscope"
OPTIONAL_LIBRARIES="telnetsrv enbscope uescope nrscope ldpc_cuda ldpc_t2 ldpc_xdma websrv oai_iqplayer imscope imscope_record"
TARGET_LIST=""
BUILD_TOOL_OPT="-j$(nproc)"
......
......@@ -701,7 +701,7 @@ int main( int argc, char **argv ) {
wait_RUs();
// once all RUs are ready initialize the rest of the gNBs ((dependence on final RU parameters after configuration)
if (IS_SOFTMODEM_DOSCOPE || IS_SOFTMODEM_IMSCOPE_ENABLED) {
if (IS_SOFTMODEM_DOSCOPE || IS_SOFTMODEM_IMSCOPE_ENABLED || IS_SOFTMODEM_IMSCOPE_RECORD_ENABLED) {
sleep(1);
scopeParms_t p;
p.argc = &argc;
......@@ -714,6 +714,11 @@ int main( int argc, char **argv ) {
if (IS_SOFTMODEM_IMSCOPE_ENABLED) {
load_softscope("im", &p);
}
AssertFatal(!(IS_SOFTMODEM_IMSCOPE_ENABLED && IS_SOFTMODEM_IMSCOPE_RECORD_ENABLED),
"Data recoding and ImScope cannot be enabled at the same time\n");
if (IS_SOFTMODEM_IMSCOPE_RECORD_ENABLED) {
load_module_shlib("imscope_record", NULL, 0, &p);
}
}
if (NFAPI_MODE != NFAPI_MODE_PNF && NFAPI_MODE != NFAPI_MODE_VNF && NFAPI_MODE != NFAPI_MODE_AERIAL) {
......
......@@ -532,6 +532,11 @@ int main(int argc, char **argv)
if (IS_SOFTMODEM_IMSCOPE_ENABLED) {
load_softscope("im", PHY_vars_UE_g[0][0]);
}
AssertFatal(!(IS_SOFTMODEM_IMSCOPE_ENABLED && IS_SOFTMODEM_IMSCOPE_RECORD_ENABLED),
"Data recoding and ImScope cannot be enabled at the same time\n");
if (IS_SOFTMODEM_IMSCOPE_RECORD_ENABLED) {
load_module_shlib("imscope_record", NULL, 0, PHY_vars_UE_g[0][0]);
}
for (int inst = 0; inst < NB_UE_INST; inst++) {
LOG_I(PHY,"Intializing UE Threads for instance %d ...\n", inst);
......
......@@ -77,6 +77,7 @@ void get_common_options(configmodule_interface_t *cfg)
uint32_t noS1 = 0, nonbiot = 0;
uint32_t rfsim = 0, do_forms = 0;
uint32_t enable_imscope = 0;
uint32_t enable_imscope_record = 0;
int nfapi_index = 0;
char *logmem_filename = NULL;
......@@ -141,6 +142,10 @@ void get_common_options(configmodule_interface_t *cfg)
IS_SOFTMODEM_IMSCOPE_ENABLED = true;
}
if (enable_imscope_record) {
IS_SOFTMODEM_IMSCOPE_RECORD_ENABLED = true;
}
if (start_websrv) {
load_module_shlib("websrv", NULL, 0, NULL);
}
......
......@@ -80,6 +80,7 @@ extern "C"
#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_IMSCOPE_RECORD "Enable recording scope data to filesystem"
#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"
......@@ -186,6 +187,7 @@ extern int usrp_tx_thread;
{"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}, \
{"imscope-record" , CONFIG_HLP_IMSCOPE_RECORD,PARAMFLAG_BOOL, .uptr=&enable_imscope_record, .defintval=0, TYPE_UINT, 0}, \
}
// clang-format on
......@@ -231,6 +233,7 @@ extern int usrp_tx_thread;
{ .s5 = { NULL } }, \
{ .s5 = { NULL } }, \
{ .s5 = { NULL } }, \
{ .s5 = { NULL } }, \
}
// clang-format on
......@@ -285,6 +288,7 @@ extern int usrp_tx_thread;
#define IS_SOFTMODEM_5GUE (get_softmodem_optmask()->bit.SOFTMODEM_5GUE_BIT)
#define IS_SOFTMODEM_NOSTATS (get_softmodem_optmask()->bit.SOFTMODEM_NOSTATS_BIT)
#define IS_SOFTMODEM_IMSCOPE_ENABLED (get_softmodem_optmask()->bit.SOFTMODEM_IMSCOPE_BIT)
#define IS_SOFTMODEM_IMSCOPE_RECORD_ENABLED (get_softmodem_optmask()->bit.SOFTMODEM_IMSCOPE_RECORD_BIT)
typedef struct optmask_s {
union {
struct {
......@@ -304,6 +308,7 @@ typedef struct optmask_s {
uint64_t SOFTMODEM_5GUE_BIT: 1;
uint64_t SOFTMODEM_NOSTATS_BIT: 1;
uint64_t SOFTMODEM_IMSCOPE_BIT: 1;
uint64_t SOFTMODEM_IMSCOPE_RECORD_BIT : 1;
} bit;
uint64_t v; // allow to export entire bit set, force to 64 bit processor atomic size
};
......
......@@ -292,7 +292,7 @@ static inline void nr_polar_deinterleaver(uint8_t *input, uint8_t *output, uint1
* De-interleaving of coded bits implementation
* TS 138.212: Section 5.4.1.3 - Interleaving of coded bits
*/
static inline void nr_polar_rm_deinterleaving_lut(uint16_t *out, const uint E)
static inline void nr_polar_rm_deinterleaving_lut(uint16_t *out, const int E)
{
int16_t in[E];
for (uint i = 0; i < E; i++)
......
......@@ -321,6 +321,7 @@ uint32_t nr_dlsch_decoding(PHY_VARS_NR_UE *phy_vars_ue,
LOG_D(PHY, "DLSCH received nok \n");
kpiStructure.nb_nack++;
dlsch->last_iteration_cnt = dlsch->max_ldpc_iterations + 1;
UEdumpScopeData(phy_vars_ue, proc->nr_slot_rx, proc->frame_rx, "DLSCH_NACK");
}
uint8_t dmrs_Type = dlsch_config->dmrsConfigType;
......
......@@ -724,13 +724,13 @@ int nr_rx_pdsch(PHY_VARS_NR_UE *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];
total_valid_res += dl_valid_re[i];
}
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);
size_t data_size = sizeof(c16_t) * dl_valid_re[i];
UEscopeCopyUnsafe(ue, pdschRxdataF_comp, &rxdataF_comp[0][0][rx_size_symbol * i], data_size, offset, i);
offset += data_size;
}
UEunlockScopeData(ue, pdschRxdataF_comp)
......
......@@ -64,6 +64,6 @@ if (ENABLE_TESTS)
add_subdirectory(tests)
endif()
if (ENABLE_IMSCOPE)
if (ENABLE_IMSCOPE OR ENABLE_IMSCOPE_RECORD)
add_subdirectory(imscope)
endif()
if (ENABLE_IMSCOPE)
CPMAddPackage("gh:ocornut/imgui#v1.91.3-docking")
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 imscope_common.cpp imscope_init.cpp)
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})
configure_file(imgui.ini ${CMAKE_BINARY_DIR}/imscope-init.ini COPYONLY)
add_executable(imscope_iq_file_viewer imscope.cpp imscope_iq_file_viewer.cpp ../phy_scope_interface.c imscope_common.cpp)
target_link_libraries(imscope_iq_file_viewer PRIVATE imgui_glfw_backend glfw imgui_opengl_renderer OpenGL::OpenGL implot UTIL minimal_lib shlib_loader)
set_target_properties(imscope_iq_file_viewer PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}")
endif()
if (ENABLE_IMSCOPE_RECORD)
add_library(imscope_record MODULE ../phy_scope_interface.c imscope_common.cpp imscope_record.cpp)
target_link_libraries(imscope_record PRIVATE UTIL atomic)
set_target_properties(imscope_record PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
endif()
CPMAddPackage("gh:ocornut/imgui#v1.91.3-docking")
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})
configure_file(imgui.ini ${CMAKE_BINARY_DIR}/imscope-init.ini COPYONLY)
This diff is collapsed.
/*
* 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 "imscope_internal.h"
#include <chrono>
void copyDataThreadSafe(void *scopeData,
enum scopeDataType type,
void *dataIn,
int elementSz,
int colSz,
int lineSz,
int offset,
metadata *meta)
{
ImScopeDataWrapper &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.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.data.scope_graph_data = data;
scope_data.data.meta = *meta;
scope_data.data.time_taken_in_ns =
std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::high_resolution_clock::now() - start).count();
scope_data.data.scope_id = type;
scope_data.is_data_ready = true;
scope_data.write_mutex.unlock();
}
}
bool tryLockScopeData(enum scopeDataType type, int elementSz, int colSz, int lineSz, metadata *meta)
{
ImScopeDataWrapper &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.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.data.scope_graph_data = data;
scope_data.data.meta = *meta;
scope_data.data.time_taken_in_ns =
std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::high_resolution_clock::now() - start).count();
memset(scope_data.data.time_taken_in_ns_per_offset, 0, sizeof(scope_data.data.time_taken_in_ns_per_offset));
memset(scope_data.data.data_copied_per_offset, 0, sizeof(scope_data.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);
ImScopeDataWrapper &scope_data = scope_array[type];
auto start = std::chrono::high_resolution_clock::now();
scopeGraphData_t *data = scope_data.data.scope_graph_data;
uint8_t *outptr = (uint8_t *)(data + 1);
memcpy(&outptr[offset], dataIn, size);
scope_data.data.time_taken_in_ns_per_offset[copy_index] =
std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::high_resolution_clock::now() - start).count();
scope_data.data.data_copied_per_offset[copy_index] = size;
}
void unlockScopeData(enum scopeDataType type)
{
ImScopeDataWrapper &scope_data = scope_array[type];
size_t total_size = 0;
for (auto i = 0; i < MAX_OFFSETS; i++) {
scope_data.data.time_taken_in_ns += scope_data.data.time_taken_in_ns_per_offset[i];
total_size += scope_data.data.data_copied_per_offset[i];
}
if (total_size != (uint64_t)scope_data.data.scope_graph_data->dataSize) {
LOG_E(PHY,
"Scope is missing data - not all data that was expected was copied - possibly missed copyDataUnsafeWithOffset call. "
"scope = %s, total_size = %lu, dataSize = %d\n",
scope_id_to_string(type),
total_size,
scope_data.data.scope_graph_data->dataSize);
}
scope_data.data.scope_id = type;
scope_data.is_data_ready = true;
scope_data.write_mutex.unlock();
}
const char *scope_id_to_string(scopeDataType type) {
switch (type) {
case pbchDlChEstimateTime:
return "pbchDlChEstimateTime";
case pbchLlr:
return "pbchLlr";
case pbchRxdataF_comp:
return "pbchRxdataF_comp";
case pdcchLlr:
return "pdcchLlr";
case pdcchRxdataF_comp:
return "pdcchRxdataF_comp";
case pdschLlr:
return "pdschLlr";
case pdschRxdataF_comp:
return "pdschRxdataF_comp";
case commonRxdataF:
return "commonRxdataF";
case gNBRxdataF:
return "gNBRxdataF";
case psbchDlChEstimateTime:
return "psbchDlChEstimateTime";
case psbchLlr:
return "psbchLlr";
case psbchRxdataF_comp:
return "psbchRxdataF_comp";
case gNBPuschRxIq:
return "gNBPuschRxIq";
case gNBPuschLlr:
return "gNBPuschLlr";
case ueTimeDomainSamples:
return "ueTimeDomainSamples";
case ueTimeDomainSamplesBeforeSync:
return "ueTimeDomainSamplesBeforeSync";
case gNbTimeDomainSamples:
return "gNbTimeDomainSamples";
default:
return "unknown type";
}
}
std::ostream &operator<<(std::ostream &os, const ImScopeData &dt)
{
os << "dataSize: " << dt.scope_graph_data->dataSize << " elementSz: " << dt.scope_graph_data->elementSz
<< " colSz: " << dt.scope_graph_data->colSz << " lineSz: " << dt.scope_graph_data->lineSz;
os << " meta: " << dt.meta.frame << "." << dt.meta.slot
<< " scope_id: " << scope_id_to_string(static_cast<scopeDataType>(dt.scope_id));
return os;
}
/*
* 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 "imscope_internal.h"
extern "C" void imscope_autoinit(void *dataptr)
{
AssertFatal(IS_SOFTMODEM_5GUE || IS_SOFTMODEM_GNB,
"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].data.scope_graph_data = nullptr;
scope_array[i].data.meta = {-1, -1};
}
scopeData_t *scope = (scopeData_t *)calloc(1, sizeof(scopeData_t));
scope->copyData = copyDataThreadSafe;
scope->tryLockScopeData = tryLockScopeData;
scope->copyDataUnsafeWithOffset = copyDataUnsafeWithOffset;
scope->unlockScopeData = unlockScopeData;
scope->copyData = copyDataThreadSafe;
if (IS_SOFTMODEM_GNB) {
scopeParms_t *scope_params = (scopeParms_t *)dataptr;
scope_params->gNB->scopeData = scope;
scope_params->ru->scopeData = scope;
} else {
PHY_VARS_NR_UE *ue = (PHY_VARS_NR_UE *)dataptr;
ue->scopeData = scope;
}
pthread_t thread;
threadCreate(&thread, imscope_thread, dataptr, (char *)"imscope", -1, sched_get_priority_min(SCHED_RR));
}
/*
* 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 IMSCOPE_INTERNAL_H
#define IMSCOPE_INTERNAL_H
#include <mutex>
#include <fstream>
#include <vector>
#include <fstream>
#include "openair1/PHY/defs_nr_UE.h"
extern "C" {
#include "../phy_scope_interface.h"
}
#define MAX_OFFSETS 14
const char *scope_id_to_string(scopeDataType type);
typedef struct ImScopeData {
scopeGraphData_t *scope_graph_data;
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];
int scope_id;
void dump_to_file(std::ofstream& stream) {
stream.write((char*)scope_graph_data, sizeof(*scope_graph_data));
stream.write((char*)&meta, sizeof(meta));
stream.write((char*)&scope_id, sizeof(scope_id));
char *source = (char *)(scope_graph_data + 1);
stream.write(source, scope_graph_data->dataSize);
}
void read_from_file(std::ifstream& stream) {
scopeGraphData_t temp;
stream.read((char*)&temp, sizeof(temp));
stream.read((char*)&meta, sizeof(meta));
stream.read((char*)&scope_id, sizeof(scope_id));
scope_graph_data = static_cast<scopeGraphData_t *>(malloc(temp.dataSize + sizeof(*scope_graph_data)));
*scope_graph_data = temp;
char *dest = (char *)(scope_graph_data + 1);
stream.read(dest, temp.dataSize);
}
friend std::ostream& operator<<(std::ostream& os, const ImScopeData& dt);
} 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;
typedef struct ImScopeDataWrapper {
std::mutex write_mutex;
bool is_data_ready;
ImScopeData data;
} ImScopeDataWrapper;
typedef struct IQData {
std::vector<int16_t> real;
std::vector<int16_t> imag;
int16_t max_iq;
std::vector<float> power;
float max_power;
float timestamp;
int nonzero_count;
int len = 0;
metadata meta;
int scope_id;
bool TryCollect(ImScopeDataWrapper *imscope_data_wrapper, float time, float epsilon, MovingAverageTimer &iq_procedure_timer)
{
if (imscope_data_wrapper->is_data_ready) {
Collect(&imscope_data_wrapper->data, time, epsilon);
iq_procedure_timer.Add(imscope_data_wrapper->data.time_taken_in_ns);
imscope_data_wrapper->is_data_ready = false;
return true;
}
return false;
}
void Collect(ImScopeData *scope_data, float time, float epsilon) {
timestamp = time;
scopeGraphData_t *iq_header = scope_data->scope_graph_data;
len = iq_header->lineSz;
real.resize(len);
imag.resize(len);
power.resize(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 = scope_data->meta;
scope_id = scope_data->scope_id;
}
} IQData;
extern ImScopeDataWrapper scope_array[EXTRA_SCOPE_TYPES];
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);
void dumpScopeData(int slot, int frame, const char *cause_string);
void *imscope_record_thread(void *data_void_ptr);
void *imscope_thread(void *data_void_ptr);
#endif
/*
* 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 <stdint.h>
#include <vector>
#include <iostream>
#include "imscope_internal.h"
static extended_kpi_ue kpiStructure;
extern "C" {
static softmodem_params_t softmodem_params;
softmodem_params_t *get_softmodem_params(void)
{
return &softmodem_params;
}
extended_kpi_ue *getKPIUE(void)
{
return &kpiStructure;
}
}
void *imscope_thread(void *data_void_ptr);
std::vector<IQData> LoadFromFile(char *filename) {
std::ifstream file(filename, std::ios::binary);
std::vector<IQData> iq_data;
if (file.is_open()) {
while (file.peek() != EOF) {
ImScopeData source;
source.read_from_file(file);
std::cout << "Loaded from IQ from file: " << source << std::endl;
IQData iq;
iq.Collect(&source, 0, 0);
iq_data.push_back(iq);
}
}
return iq_data;
}
int main(int argc, char **argv)
{
if (argc < 2) {
printf("Usage: %s <filename>\n", argv[0]);
return 1;
}
std::vector<IQData> iq_data = LoadFromFile(argv[1]);
if (iq_data.size() == 0) {
printf("Error: No IQ data found in file\n");
return 1;
}
imscope_thread(static_cast<void *>(&iq_data));
}
/*
* 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 "imscope_internal.h"
#include "openair1/PHY/TOOLS/phy_scope_interface.h"
#include <sys/stat.h>
#include <errno.h>
#include <atomic>
#include <new>
#ifdef __cpp_lib_hardware_interference_size
using std::hardware_constructive_interference_size;
using std::hardware_destructive_interference_size;
#else
// 64 bytes on x86-64 │ L1_CACHE_BYTES │ L1_CACHE_SHIFT │ __cacheline_aligned │ ...
constexpr std::size_t hardware_constructive_interference_size = 64;
constexpr std::size_t hardware_destructive_interference_size = 64;
#endif
#define NUM_CACHED_DATA 4
// Set maximum number of kilobytes written per recording session to 1GB. This is a safeguard against
// recoding thread writing too many files and exhausting disk space.
#define MAX_KBYTES_WRITTEN_PER_SESSION (1000 * 1000)
ImScopeDataWrapper scope_array[EXTRA_SCOPE_TYPES];
typedef struct alignas(hardware_destructive_interference_size) ImScopeDumpInstruction {
int slot;
int frame;
const char *cause;
} ImScopeDumpInstruction;
std::atomic<ImScopeDumpInstruction> dump_instruction;
ImScopeDumpInstruction void_instruction({-1, -1, nullptr});
extern "C" void imscope_record_autoinit(void *dataptr)
{
AssertFatal(IS_SOFTMODEM_5GUE || IS_SOFTMODEM_GNB,
"Scope cannot find NRUE or GNB context");
LOG_A(PHY, "Imscope recording started\n");
for (auto i = 0U; i < EXTRA_SCOPE_TYPES; i++) {
scope_array[i].is_data_ready = false;
scope_array[i].data.scope_graph_data = nullptr;
scope_array[i].data.meta = {-1, -1};
}
dump_instruction.exchange(void_instruction);
scopeData_t *scope = (scopeData_t *)calloc(1, sizeof(scopeData_t));
scope->copyData = copyDataThreadSafe;
scope->tryLockScopeData = tryLockScopeData;
scope->copyDataUnsafeWithOffset = copyDataUnsafeWithOffset;
scope->unlockScopeData = unlockScopeData;
scope->copyData = copyDataThreadSafe;
scope->dumpScopeData = dumpScopeData;
if (IS_SOFTMODEM_GNB) {
scopeParms_t *scope_params = (scopeParms_t *)dataptr;
scope_params->gNB->scopeData = scope;
scope_params->ru->scopeData = scope;
} else {
PHY_VARS_NR_UE *ue = (PHY_VARS_NR_UE *)dataptr;
ue->scopeData = scope;
}
pthread_t thread;
threadCreate(&thread, imscope_record_thread, dataptr, (char *)"imscope_record", -1, sched_get_priority_min(SCHED_RR));
}
void dumpScopeData(int slot, int frame, const char *cause_string)
{
ImScopeDumpInstruction to_dump = {slot, frame, cause_string};
dump_instruction.compare_exchange_weak(void_instruction, to_dump);
}
void *imscope_record_thread(void *data_void_ptr)
{
const char *path = "imscope-dump";
struct stat info;
bool has_dir = false;
if (stat(path, &info) == 0) {
if (info.st_mode & S_IFDIR) {
has_dir = true;
}
}
if (has_dir) {
LOG_A(PHY, "ImScope recorder thread starts, recording data to %s\n", path);
} else {
LOG_A(PHY, "ImScope recorder thread starts, recording data to cwd\n");
}
std::string program_name(program_invocation_name);
size_t last_slash = program_name.find_last_of("/");
if (last_slash != std::string::npos) {
program_name = program_name.substr(last_slash + 1);
}
ImScopeData scope_cache[EXTRA_SCOPE_TYPES][NUM_CACHED_DATA];
memset(scope_cache, 0, sizeof(scope_cache));
uint scope_cache_index[EXTRA_SCOPE_TYPES] = {0};
float kbytes_written = 0.0f;
while (kbytes_written < MAX_KBYTES_WRITTEN_PER_SESSION) {
ImScopeDumpInstruction dump = dump_instruction.load(std::memory_order::memory_order_relaxed);
if (dump.slot != -1) {
std::string filename;
if (has_dir) {
filename += std::string(path) + "/";
}
filename += program_name + std::string("-") + dump.cause + std::string("-");
filename += std::to_string(dump.frame) + std::string("-") + std::to_string(dump.slot) + std::string(".imscope");
std::ofstream outf(filename, std::ios_base::out | std::ios_base::binary);
if (!outf) {
LOG_E(PHY, "Could not open file %s for writing\n", filename.c_str());
break;
}
for (auto i = 0U; i < EXTRA_SCOPE_TYPES; i++) {
for (auto j = 0U; j < NUM_CACHED_DATA; j++) {
ImScopeData &scope_data = scope_cache[i][j];
if (scope_data.meta.frame == dump.frame && scope_data.meta.slot == dump.slot) {
scope_data.dump_to_file(outf);
}
}
}
long pos = outf.tellp();
kbytes_written += pos / 1000.0f;
outf.close();
LOG_D(PHY, "ImScope done writing to %s\n", filename.c_str());
dump_instruction.exchange(void_instruction);
}
for (auto i = 0U; i < EXTRA_SCOPE_TYPES; i++) {
ImScopeDataWrapper &scope_data = scope_array[i];
if (scope_data.is_data_ready) {
uint cache_index = scope_cache_index[i]++ % NUM_CACHED_DATA;
ImScopeData tmp = scope_cache[i][cache_index];
scope_cache[i][cache_index] = scope_data.data;
scope_data.data = tmp;
scope_data.is_data_ready = false;
}
}
}
LOG_E(PHY, "ImScope recorder thread exits!\n");
return nullptr;
}
......@@ -112,6 +112,7 @@ typedef struct scopeData_s {
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);
void (*dumpScopeData)(int slot, int frame, const char *cause_string);
} scopeData_t;
int load_softscope(char *exectype, void *initarg);
......@@ -157,6 +158,14 @@ void copyData(void *, enum scopeDataType type, void *dataIn, int elementSz, int
scope_data->unlockScopeData(type); \
}
#define gNBdumpScopeData(gnb, slot, frame, cause_string) \
do { \
scopeData_t *scope_data = (scopeData_t *)gnb->scopeData; \
if (scope_data && scope_data->dumpScopeData) { \
scope_data->dumpScopeData((slot), (frame), (cause_string)); \
} \
} while (0)
#define UEScopeHasTryLock(ue) \
(ue->scopeData && ((scopeData_t *)ue->scopeData)->tryLockScopeData)
......@@ -176,6 +185,20 @@ void copyData(void *, enum scopeDataType type, void *dataIn, int elementSz, int
scope_data->unlockScopeData(type); \
}
#define UEdumpScopeData(ue, slot, frame, cause_string) \
do { \
scopeData_t *scope_data = (scopeData_t *)ue->scopeData; \
if (scope_data && scope_data->dumpScopeData) { \
scope_data->dumpScopeData((slot), (frame), (cause_string)); \
} \
} while (0)
#ifdef __cplusplus
extern "C" {
#endif
extended_kpi_ue* getKPIUE();
#ifdef __cplusplus
}
#endif
#endif
......@@ -69,3 +69,21 @@ Run with `--imscope` flag
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.
## Recording scope data and reviewing recorded IQ files
It is possible to record scope data from the modems.
- To record IQ data, run a modem with `--imscope-record` flag.
The recording happens whenever a modem calls `dumpScopeData`, which currently is
done only in gNB on PUSCH DTX and PUSCH CRC NOK.
There is a limit of ~1GB of files written by the thread in a single run to avoid
accidentally exhausting disk space.
If a directory `imscope-dump` is available the data will be recorded there in .imscope
files. Otherwise files are written to the directory the modem was run. This is done so that
the directory can be mapped in a docker container.
- To view recorded IQ files, use the new executable `imscope_iq_file_viewer`. This can be done by running:
```
./imscope_iq_file_viewer <path_to_iq_file>
```
Replace `<path_to_iq_file>` with the path to the IQ file you want to view.
......@@ -40,6 +40,7 @@
#include "assertions.h"
#include <time.h>
#include <stdint.h>
#include <openair1/PHY/TOOLS/phy_scope_interface.h>
//#define DEBUG_RXDATA
//#define SRS_IND_DEBUG
......@@ -438,6 +439,7 @@ static int nr_ulsch_procedures(PHY_VARS_gNB *gNB, int frame_rx, int slot_rx, boo
ulsch_harq->ulsch_pdu.rb_size,
ulsch_harq->TBS);
nr_fill_indication(gNB, ulsch->frame, ulsch->slot, ULSCH_id, ulsch->harq_pid, 1, 0, crc, pdu);
gNBdumpScopeData(gNB, ulsch->slot, ulsch->frame, "ULSCH_NACK");
ulsch->handled = 1;
LOG_D(PHY, "ULSCH %d in error\n",ULSCH_id);
ulsch->last_iteration_cnt = ulsch->max_ldpc_iterations + 1; // Setting to max_ldpc_iterations + 1 is sufficient given that this variable is only used for checking for failure
......@@ -868,6 +870,7 @@ int phy_procedures_gNB_uespec_RX(PHY_VARS_gNB *gNB, int frame_rx, int slot_rx, N
nfapi_nr_rx_data_pdu_t *pdu = &UL_INFO->rx_ind.pdu_list[UL_INFO->rx_ind.number_of_pdus++];
nr_fill_indication(gNB, frame_rx, slot_rx, ULSCH_id, ulsch->harq_pid, 1, 1, crc, pdu);
pusch_DTX++;
gNBdumpScopeData(gNB, ulsch->slot, ulsch->frame, "ULSCH_DTX");
continue;
}
} else {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment