From fa72a30bf4ff7ef58307f7b68807f26a8f02c589 Mon Sep 17 00:00:00 2001 From: Merkebu Girmay Date: Fri, 6 Feb 2026 00:37:46 -0800 Subject: [PATCH 1/2] add multi-ue support to vrtsim - Add num_ues and ue_id parameters for server and client configuration - Implement ul combining and dl distribution to all ues - Each ue uses dedicated antenna offsets based on ue_id - Works with no-chanmod mode only and with uniform antenna dimensions across all ues --- radio/vrtsim/vrtsim.c | 411 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 372 insertions(+), 39 deletions(-) diff --git a/radio/vrtsim/vrtsim.c b/radio/vrtsim/vrtsim.c index 9a0cd779dc..36780f3ae6 100644 --- a/radio/vrtsim/vrtsim.c +++ b/radio/vrtsim/vrtsim.c @@ -44,6 +44,7 @@ #include #include #include +#include #include "common_lib.h" #include "shm_td_iq_channel.h" #include "SIMULATION/TOOLS/sim.h" @@ -59,6 +60,7 @@ typedef enum { ROLE_SERVER = 1, ROLE_CLIENT } role; #define MAX_NUM_ANTENNAS_TX 4 #define SAVED_SAMPLES_LEN 256 +#define MAX_NUM_UES MAX_MOBILES_PER_GNB #define ROLE_CLIENT_STRING "client" #define ROLE_SERVER_STRING "server" @@ -90,6 +92,8 @@ typedef enum { ROLE_SERVER = 1, ROLE_CLIENT } role; {"cirdb_model_id", "Preferred TDL model id 0..4", 0, .iptr = &vrtsim_state->cirdb_model_id, .defintval = 0, TYPE_INT, 0}, \ {"cirdb_ds_ns", "Desired RMS delay spread in ns", 0, .dblptr = &vrtsim_state->cirdb_ds_ns, .defdblval = 10.0, TYPE_DOUBLE, 0}, \ {"cirdb_speed_mps", "Desired speed in m/s", 0, .dblptr = &vrtsim_state->cirdb_speed_mps, .defdblval = 1.5, TYPE_DOUBLE, 0}, \ + {"num_ues", "Number of UE slots (server only)\n", 0, .iptr = &vrtsim_state->num_ues, .defintval = 1, TYPE_INT, 0}, \ + {"ue_id", "UE slot index 0..num_ues-1 (client only)\n", 0, .iptr = &vrtsim_state->ue_id, .defintval = 0, TYPE_INT, 0}, \ }; // clang-format on @@ -113,6 +117,20 @@ typedef struct tx_timing_s { histogram_t tx_histogram; } tx_timing_t; +typedef struct { + int model_id; + double ds_ns; + double speed_mps; +} cirdb_conf_t; + +typedef struct { + int tx_ant; + int rx_ant; + int tx_offset; + int rx_offset; + cirdb_conf_t cir_conf; +} ue_conf_t; + typedef struct { int role; char *connection_descriptor; @@ -133,6 +151,8 @@ typedef struct { int tx_num_channels; int rx_num_channels; channel_desc_t *channel_desc; + channel_desc_t *channel_desc_per_ue[MAX_NUM_UES]; + pthread_mutex_t cirdb_mutex; Actor_t *channel_modelling_actors; char *taps_socket; int client_num_rx_antennas; @@ -145,6 +165,18 @@ typedef struct { int cirdb_model_id; double cirdb_ds_ns; double cirdb_speed_mps; + // Multi-UE support + int num_ues; + int ue_id; + + ue_conf_t ue_conf[MAX_NUM_UES]; + ue_conf_t ue; + + // Totals + int total_ul_streams; + int total_dl_streams; + sample_t *ul_combine_buffer; + size_t ul_combine_buffer_len; } vrtsim_state_t; static void histogram_add(histogram_t *histogram, double diff) @@ -251,10 +283,11 @@ static void *vrtsim_timing_job(void *arg) } typedef struct client_info_s { - int server_num_rx_antennas; - int client_num_rx_antennas; + int num_ues; + int gnb_num_tx_ant; + int gnb_num_rx_ant; + ue_conf_t ues[MAX_NUM_UES]; } client_info_t; - /** * @brief Publishes the client information information to a file for the client to read. * @@ -293,43 +326,174 @@ static client_info_t client_read_info(char *descriptor_file) return client_info; } +static void parse_ue_config(vrtsim_state_t *vrtsim_state) +{ + AssertFatal(vrtsim_state->num_ues > 0 && vrtsim_state->num_ues <= MAX_NUM_UES, + "num_ues=%d out of range (1..%d)\n", vrtsim_state->num_ues, MAX_NUM_UES); + + for (int i = 0; i < vrtsim_state->num_ues; i++) { + char prefix[64]; + snprintf(prefix, sizeof(prefix), "%s.ue_config.[%d]", VRTSIM_SECTION, i); + + char *antennas = NULL; + int model_id = vrtsim_state->cirdb_model_id; + double ds_ns = vrtsim_state->cirdb_ds_ns; + double speed_mps = vrtsim_state->cirdb_speed_mps; + + paramdef_t ue_params[] = { + {"antennas", "Antenna config e.g. \"1x2\"", 0, .strptr = &antennas, .defstrval = NULL, TYPE_STRING, 0}, + {"model_id", "TDL model id 0..4", 0, .iptr = &model_id, .defintval = vrtsim_state->cirdb_model_id, TYPE_INT, 0}, + {"ds_ns", "Delay spread in ns", 0, .dblptr = &ds_ns, .defdblval = vrtsim_state->cirdb_ds_ns, TYPE_DOUBLE, 0}, + {"speed_mps", "Speed in m/s", 0, .dblptr = &speed_mps, .defdblval = vrtsim_state->cirdb_speed_mps, TYPE_DOUBLE, 0}, + }; + config_get(config_get_if(), ue_params, sizeofArray(ue_params), prefix); + + if (antennas != NULL) { + int tx_ant, rx_ant; + AssertFatal(sscanf(antennas, "%dx%d", &tx_ant, &rx_ant) == 2, + "Invalid antenna format '%s' for UE %d, use e.g. '1x2'\n", antennas, i); + AssertFatal(tx_ant > 0 && tx_ant <= MAX_NUM_ANTENNAS_TX, + "Invalid TX antenna count %d for UE %d\n", tx_ant, i); + AssertFatal(rx_ant > 0 && rx_ant <= MAX_NUM_ANTENNAS_TX, + "Invalid RX antenna count %d for UE %d\n", rx_ant, i); + vrtsim_state->ue_conf[i].tx_ant = tx_ant; + vrtsim_state->ue_conf[i].rx_ant = rx_ant; + } else { + vrtsim_state->ue_conf[i].tx_ant = 1; + vrtsim_state->ue_conf[i].rx_ant = 1; + } + + AssertFatal(model_id >= 0 && model_id <= 4, + "Invalid model_id %d for UE %d (must be 0-4)\n", model_id, i); + AssertFatal(ds_ns > 0, + "Invalid ds_ns %.1f for UE %d (must be > 0)\n", ds_ns, i); + AssertFatal(speed_mps >= 0, + "Invalid speed_mps %.1f for UE %d (must be >= 0)\n", speed_mps, i); + + vrtsim_state->ue_conf[i].cir_conf.model_id = model_id; + vrtsim_state->ue_conf[i].cir_conf.ds_ns = ds_ns; + vrtsim_state->ue_conf[i].cir_conf.speed_mps = speed_mps; + + LOG_I(HW, "VRTSIM: UE %d - %dx%d antennas, Model %d (TDL-%c), DS %.1fns, Speed %.1fm/s\n", + i, + vrtsim_state->ue_conf[i].tx_ant, + vrtsim_state->ue_conf[i].rx_ant, + model_id, 'A' + model_id, ds_ns, speed_mps); + } +} + +static void compute_ue_antenna_offsets(vrtsim_state_t *vrtsim_state) +{ + int tx_offset = 0; + int rx_offset = 0; + + for (int i = 0; i < vrtsim_state->num_ues; i++) { + vrtsim_state->ue_conf[i].tx_offset = tx_offset; + vrtsim_state->ue_conf[i].rx_offset = rx_offset; + + LOG_I(HW, + "VRTSIM: UE %d - TX: %d antennas (offset %d), RX: %d antennas (offset %d)\n", + i, + vrtsim_state->ue_conf[i].tx_ant, + tx_offset, + vrtsim_state->ue_conf[i].rx_ant, + rx_offset); + + tx_offset += vrtsim_state->ue_conf[i].tx_ant; + rx_offset += vrtsim_state->ue_conf[i].rx_ant; + } + + vrtsim_state->total_ul_streams = tx_offset; + vrtsim_state->total_dl_streams = rx_offset; + + LOG_I(HW, "VRTSIM: Total streams - UL: %d, DL: %d\n", vrtsim_state->total_ul_streams, vrtsim_state->total_dl_streams); +} + static int vrtsim_connect(openair0_device_t *device) { vrtsim_state_t *vrtsim_state = (vrtsim_state_t *)device->priv; // Setup a shared memory channel if (vrtsim_state->role == ROLE_SERVER) { - vrtsim_state->peer_info.num_rx_antennas = vrtsim_state->client_num_rx_antennas; - vrtsim_state->channel = shm_td_iq_channel_create(DEFAULT_CHANNEL_NAME, - vrtsim_state->peer_info.num_rx_antennas, - device->openair0_cfg[0].rx_num_channels); - // Exchange peer info - client_info_t client_info = { - .server_num_rx_antennas = device->openair0_cfg[0].rx_num_channels, - .client_num_rx_antennas = vrtsim_state->client_num_rx_antennas, - }; - server_publish_client_info(client_info, vrtsim_state->connection_descriptor); + parse_ue_config(vrtsim_state); + compute_ue_antenna_offsets(vrtsim_state); + int ul_streams = (vrtsim_state->chanmod || vrtsim_state->taps_socket || vrtsim_state->use_cirdb) + ? vrtsim_state->num_ues * device->openair0_cfg[0].rx_num_channels + : vrtsim_state->total_ul_streams; + vrtsim_state->channel = + shm_td_iq_channel_create(DEFAULT_CHANNEL_NAME, ul_streams, vrtsim_state->total_dl_streams); + size_t max_nsamps = 7680 * 2; + vrtsim_state->ul_combine_buffer = calloc_or_fail(max_nsamps, sizeof(sample_t)); + vrtsim_state->ul_combine_buffer_len = max_nsamps; + // Exchange peer info + + client_info_t client_info = { + .num_ues = vrtsim_state->num_ues, + .gnb_num_tx_ant = device->openair0_cfg[0].tx_num_channels, + .gnb_num_rx_ant = device->openair0_cfg[0].rx_num_channels, + }; + + for (int i = 0; i < vrtsim_state->num_ues; i++) + client_info.ues[i] = vrtsim_state->ue_conf[i]; + + server_publish_client_info(client_info, vrtsim_state->connection_descriptor); + vrtsim_state->peer_info.num_rx_antennas = vrtsim_state->client_num_rx_antennas; + vrtsim_state->last_received_sample = shm_td_iq_channel_get_current_sample(vrtsim_state->channel); vrtsim_state->run_timing_thread = true; threadCreate(&vrtsim_state->timing_thread, vrtsim_timing_job, vrtsim_state, "vrtsim_timing", -1, OAI_PRIORITY_RT_MAX); } else { client_info_t client_info = client_read_info(vrtsim_state->connection_descriptor); - AssertFatal(client_info.server_num_rx_antennas > 0, "Server did not publish valid client info, aborting client connection\n"); - AssertFatal( - client_info.client_num_rx_antennas == device->openair0_cfg[0].rx_num_channels, - "Server expects different number of RX antennas. %d != %d. Use server command line option --client-num-rx-antennas\n", - client_info.client_num_rx_antennas, - device->openair0_cfg[0].rx_num_channels); + AssertFatal(client_info.num_ues > 0, "Server did not publish valid num_ues\n"); + AssertFatal(vrtsim_state->ue_id < client_info.num_ues, "ue_id %d >= num_ues %d\n", vrtsim_state->ue_id, client_info.num_ues); + + vrtsim_state->ue = client_info.ues[vrtsim_state->ue_id]; + + AssertFatal(vrtsim_state->ue.tx_ant == device->openair0_cfg[0].tx_num_channels, + "Server expects UE %d to have %d TX antennas, this UE has %d\n", + vrtsim_state->ue_id, + vrtsim_state->ue.tx_ant, + device->openair0_cfg[0].tx_num_channels); + AssertFatal(vrtsim_state->ue.rx_ant == device->openair0_cfg[0].rx_num_channels, + "Server expects UE %d to have %d RX antennas, this UE has %d\n", + vrtsim_state->ue_id, + vrtsim_state->ue.rx_ant, + device->openair0_cfg[0].rx_num_channels); + + LOG_I(HW, + "VRTSIM: UE %d/%d - %dx%d antennas, UL offset=%d, DL offset=%d\n", + vrtsim_state->ue_id, + client_info.num_ues, + vrtsim_state->ue.tx_ant, + vrtsim_state->ue.rx_ant, + vrtsim_state->ue.tx_offset, + vrtsim_state->ue.rx_offset); + if (vrtsim_state->use_cirdb) { + vrtsim_state->cirdb_model_id = vrtsim_state->ue.cir_conf.model_id; + vrtsim_state->cirdb_ds_ns = vrtsim_state->ue.cir_conf.ds_ns; + vrtsim_state->cirdb_speed_mps = vrtsim_state->ue.cir_conf.speed_mps; + + LOG_I(HW, + "VRTSIM: UE %d channel - Model %d (TDL-%c), DS %.1fns, Speed %.1fm/s\n", + vrtsim_state->ue_id, + vrtsim_state->cirdb_model_id, + 'A' + vrtsim_state->cirdb_model_id, + vrtsim_state->cirdb_ds_ns, + vrtsim_state->cirdb_speed_mps); + } vrtsim_state->channel = shm_td_iq_channel_connect(DEFAULT_CHANNEL_NAME, 10); - vrtsim_state->peer_info.num_rx_antennas = client_info.server_num_rx_antennas; + vrtsim_state->peer_info.num_rx_antennas = client_info.gnb_num_rx_ant; vrtsim_state->last_received_sample = shm_td_iq_channel_get_current_sample(vrtsim_state->channel); } // Handle channel modelling after number of RX antennas are known int num_tx_stats = 1; if (vrtsim_state->chanmod || vrtsim_state->taps_socket || vrtsim_state->use_cirdb) { - vrtsim_state->channel_modelling_actors = calloc_or_fail(vrtsim_state->peer_info.num_rx_antennas, sizeof(Actor_t)); - for (int i = 0; i < vrtsim_state->peer_info.num_rx_antennas; i++) { + int num_actors = (vrtsim_state->role == ROLE_SERVER && vrtsim_state->num_ues > 1) ? vrtsim_state->total_dl_streams + : vrtsim_state->peer_info.num_rx_antennas; + + vrtsim_state->channel_modelling_actors = calloc_or_fail(num_actors, sizeof(Actor_t)); + for (int i = 0; i < num_actors; i++) { init_actor(&vrtsim_state->channel_modelling_actors[i], "chanmod", -1); } if (vrtsim_state->taps_socket) { @@ -379,16 +543,55 @@ static int vrtsim_connect(openair0_device_t *device) sel.yaml_path ? sel.yaml_path : "(auto)", sel.bin_path ? sel.bin_path : "(auto)"); - cirdb_connect(0, - device->openair0_cfg[0].tx_num_channels, - vrtsim_state->peer_info.num_rx_antennas, - &sel, - &vrtsim_state->channel_desc); - LOG_A(HW, "VRTSIM: channel taps via CIR DB\n"); + // Multi-UE server: create per-UE channel descriptors + if (vrtsim_state->role == ROLE_SERVER && vrtsim_state->num_ues > 1) { + for (int u = 0; u < vrtsim_state->num_ues; u++) { + cirdb_select_opts_t ue_sel = sel; + ue_sel.want_model_id = vrtsim_state->ue_conf[u].cir_conf.model_id; + ue_sel.want_ds_ns = vrtsim_state->ue_conf[u].cir_conf.ds_ns; + ue_sel.want_speed_mps = vrtsim_state->ue_conf[u].cir_conf.speed_mps; + + AssertFatal(ue_sel.want_model_id >= 0 && ue_sel.want_model_id <= 4, + "Invalid CIRDB model_id=%d for UE %d\n", + ue_sel.want_model_id, + u); + + cirdb_connect(u, + device->openair0_cfg[0].tx_num_channels, + vrtsim_state->ue_conf[u].rx_ant, + &ue_sel, + &vrtsim_state->channel_desc_per_ue[u]); + + channel_desc_t *cd = vrtsim_state->channel_desc_per_ue[u]; + AssertFatal(cd != NULL, "CIRDB failed to create channel_desc for UE %d\n", u); + AssertFatal(cd->nb_tx == device->openair0_cfg[0].tx_num_channels, + "CIRDB shape mismatch UE%d: nb_tx=%d expected %d\n", + u, cd->nb_tx, device->openair0_cfg[0].tx_num_channels); + LOG_I(HW, "VRTSIM: UE %d channel_desc=%p ch_ps=%p ch=%p nb_tx=%d nb_rx=%d\n", + u, cd, cd->ch_ps, cd->ch, cd->nb_tx, cd->nb_rx); + LOG_I(HW, "VRTSIM: UE %d CIRDB - Model %d (TDL-%c), antennas %dx%d, nb_rx=%d\n", + u, ue_sel.want_model_id, 'A' + ue_sel.want_model_id, + device->openair0_cfg[0].tx_num_channels, vrtsim_state->ue_conf[u].rx_ant, cd->nb_rx); + } + vrtsim_state->channel_desc = vrtsim_state->channel_desc_per_ue[0]; + pthread_mutex_init(&vrtsim_state->cirdb_mutex, NULL); + LOG_A(HW, "VRTSIM: Multi-UE channel taps via CIR DB\n"); + } else { + cirdb_connect(0, + device->openair0_cfg[0].tx_num_channels, + vrtsim_state->peer_info.num_rx_antennas, + &sel, + &vrtsim_state->channel_desc); + LOG_A(HW, "VRTSIM: channel taps via CIR DB\n"); + } } else { load_channel_model(vrtsim_state); } num_tx_stats = vrtsim_state->peer_info.num_rx_antennas; + if (vrtsim_state->role == ROLE_SERVER && vrtsim_state->num_ues > 1) { + vrtsim_state->peer_info.num_rx_antennas = vrtsim_state->total_dl_streams; + num_tx_stats = vrtsim_state->total_dl_streams; + } } vrtsim_state->tx_timing = calloc_or_fail(num_tx_stats, sizeof(tx_timing_t)); for (int i = 0; i < num_tx_stats; i++) { @@ -451,15 +654,69 @@ static void perform_channel_modelling(void *arg) c16_t **input_samples = (c16_t **)channel_modelling_args->samples; channel_desc_t *channel_desc = vrtsim_state->channel_desc; + int local_aarx = aarx; + int global_aarx = aarx; + int target_ue = 0; + + if (vrtsim_state->role == ROLE_CLIENT) { + global_aarx = vrtsim_state->ue_id * vrtsim_state->peer_info.num_rx_antennas + aarx; + } + + if (vrtsim_state->role == ROLE_SERVER && vrtsim_state->num_ues > 1) { + if (aarx >= vrtsim_state->total_dl_streams) { + LOG_E(HW, "VRTSIM: aarx=%d >= total_dl_streams=%d\n", aarx, vrtsim_state->total_dl_streams); + return; + } + + bool found = false; + for (int u = 0; u < vrtsim_state->num_ues; u++) { + int offset = vrtsim_state->ue_conf[u].rx_offset; + int count = vrtsim_state->ue_conf[u].rx_ant; + if (aarx >= offset && aarx < offset + count) { + target_ue = u; + local_aarx = aarx - offset; + channel_desc = vrtsim_state->channel_desc_per_ue[u]; + found = true; + break; + } + } + + if (!found) { + LOG_E(HW, "VRTSIM: aarx=%d could not map to any UE (total_dl_streams=%d)\n", aarx, vrtsim_state->total_dl_streams); + return; + } + } if (channel_desc == NULL) { + LOG_E(HW, "VRTSIM: channel_desc is NULL for aarx=%d\n", aarx); + return; + } + + // Bounds check: ensure local_aarx is within channel_desc bounds + if (local_aarx >= channel_desc->nb_rx) { + LOG_E(HW, + "VRTSIM: UE%d local_aarx=%d >= nb_rx=%d (global aarx=%d). CIRDB shape mismatch.\n", + target_ue, + local_aarx, + channel_desc->nb_rx, + aarx); return; } if (vrtsim_state->use_cirdb) { double seconds = (double)channel_modelling_args->timestamp / vrtsim_state->sample_rate; uint64_t elapsed_ns = (uint64_t)(seconds * 1e9 + 0.5); + + // Lock for multi-UE to prevent concurrent CIRDB updates + if (vrtsim_state->role == ROLE_SERVER && vrtsim_state->num_ues > 1) { + pthread_mutex_lock(&vrtsim_state->cirdb_mutex); + } + cirdb_update(elapsed_ns); + + if (vrtsim_state->role == ROLE_SERVER && vrtsim_state->num_ues > 1) { + pthread_mutex_unlock(&vrtsim_state->cirdb_mutex); + } } cf_t channel_impulse_response[nb_tx_ant][channel_desc->channel_length]; @@ -468,7 +725,7 @@ static void perform_channel_modelling(void *arg) const float pathloss_linear = powf(10, channel_desc->path_loss_dB / 20.0); // Convert channel impulse response to float + apply pathloss for (int aatx = 0; aatx < nb_tx_ant; aatx++) { - const struct complexd *channelModel = channel_desc->ch[aarx + (aatx * channel_desc->nb_rx)]; + const struct complexd *channelModel = channel_desc->ch[local_aarx + (aatx * channel_desc->nb_rx)]; for (int i = 0; i < channel_desc->channel_length; i++) { channel_impulse_response[aatx][i].r = channelModel[i].r * pathloss_linear; channel_impulse_response[aatx][i].i = channelModel[i].i * pathloss_linear; @@ -477,7 +734,7 @@ static void perform_channel_modelling(void *arg) } } else { for (int aatx = 0; aatx < nb_tx_ant; aatx++) { - struct complexf *channelModel = channel_desc->ch_ps[aarx + (aatx * channel_desc->nb_rx)]; + struct complexf *channelModel = channel_desc->ch_ps[local_aarx + (aatx * channel_desc->nb_rx)]; channel_impulse_response_p[aatx] = channelModel; } } @@ -537,7 +794,7 @@ static void perform_channel_modelling(void *arg) channel_modelling_args->timestamp + start_sample, samples_out, num_samples, - aarx, + global_aarx, channel_modelling_args->flags, aarx); } @@ -623,8 +880,27 @@ static int vrtsim_write(openair0_device_t *device, timestamp -= device->openair0_cfg->command_line_sample_advance; vrtsim_state_t *vrtsim_state = (vrtsim_state_t *)device->priv; bool channel_modelling = vrtsim_state->chanmod || vrtsim_state->taps_socket || vrtsim_state->use_cirdb; - return channel_modelling ? vrtsim_write_with_chanmod(vrtsim_state, timestamp, samplesVoid, nsamps, nbAnt, flags) - : vrtsim_write_internal(vrtsim_state, timestamp, (c16_t *)samplesVoid[0], nsamps, 0, flags, 0); + if (channel_modelling) { + return vrtsim_write_with_chanmod(vrtsim_state, timestamp, samplesVoid, nsamps, nbAnt, flags); + } + if (vrtsim_state->role == ROLE_CLIENT) { + for (int aatx = 0; aatx < nbAnt; aatx++) { + int global_ul_ant = vrtsim_state->ue.tx_offset + aatx; + vrtsim_write_internal(vrtsim_state, timestamp, (c16_t *)samplesVoid[aatx], nsamps, global_ul_ant, flags, 0); + } + return nsamps; + } else { + AssertFatal(nbAnt == 1, "Multi-TX gNB not yet supported in multi-UE no-chanmod mode\n"); + for (int u = 0; u < vrtsim_state->num_ues; u++) { + int rx_offset = vrtsim_state->ue_conf[u].rx_offset; + int num_rx_ant = vrtsim_state->ue_conf[u].rx_ant; + for (int aarx = 0; aarx < num_rx_ant; aarx++) { + int global_dl_ant = rx_offset + aarx; + vrtsim_write_internal(vrtsim_state, timestamp, (c16_t *)samplesVoid[0], nsamps, global_dl_ant, flags, 0); + } + } + return nsamps; + } } static int vrtsim_write_beams(openair0_device_t *device, @@ -665,12 +941,62 @@ static int vrtsim_read(openair0_device_t *device, openair0_timestamp_t *ptimesta } } } - - int ret = shm_td_iq_channel_rx(vrtsim_state->channel, vrtsim_state->last_received_sample, nsamps, 0, samplesVoid[0]); - if (ret == CHANNEL_ERROR_TOO_LATE) { - vrtsim_state->rx_samples_late += nsamps; - } else if (ret == CHANNEL_ERROR_TOO_EARLY) { - vrtsim_state->rx_early += 1; + if (vrtsim_state->role == ROLE_SERVER) { + if (vrtsim_state->num_ues > 1) { + /* Multi-UE UL combining */ + if (nsamps > vrtsim_state->ul_combine_buffer_len) { + sample_t *tmp = realloc(vrtsim_state->ul_combine_buffer, nsamps * sizeof(sample_t)); + AssertFatal(tmp != NULL, "Failed to realloc UL combine buffer\n"); + vrtsim_state->ul_combine_buffer = tmp; + vrtsim_state->ul_combine_buffer_len = nsamps; + } + for (int aarx = 0; aarx < nbAnt; aarx++) + memset(samplesVoid[aarx], 0, nsamps * sizeof(sample_t)); + for (int u = 0; u < vrtsim_state->num_ues; u++) { + for (int aarx = 0; aarx < nbAnt; aarx++) { + int stream = u * nbAnt + aarx; + int ret = shm_td_iq_channel_rx(vrtsim_state->channel, + vrtsim_state->last_received_sample, + nsamps, + stream, + vrtsim_state->ul_combine_buffer); + if (ret == CHANNEL_ERROR_TOO_LATE) { + vrtsim_state->rx_samples_late += nsamps; + } else if (ret == CHANNEL_ERROR_TOO_EARLY) { + vrtsim_state->rx_early += 1; + } + int16_t *out = (int16_t *)samplesVoid[aarx]; + int16_t *in = (int16_t *)vrtsim_state->ul_combine_buffer; + for (int i = 0; i < nsamps * 2; i++) { + int32_t sum = (int32_t)out[i] + (int32_t)in[i]; + out[i] = (int16_t)((sum > 32767) ? 32767 : (sum < -32768) ? -32768 : sum); + } + } + } + } else { + /* Single-UE server UL read */ + int ret = shm_td_iq_channel_rx(vrtsim_state->channel, vrtsim_state->last_received_sample, nsamps, 0, samplesVoid[0]); + if (ret == CHANNEL_ERROR_TOO_LATE) { + vrtsim_state->rx_samples_late += nsamps; + } else if (ret == CHANNEL_ERROR_TOO_EARLY) { + vrtsim_state->rx_early += 1; + } + for (int aarx = 1; aarx < nbAnt; aarx++) { + if (samplesVoid[aarx] != NULL) + memcpy(samplesVoid[aarx], samplesVoid[0], nsamps * sizeof(sample_t)); + } + } + } else { + for (int aarx = 0; aarx < nbAnt; aarx++) { + int global_dl_ant = vrtsim_state->ue.rx_offset + aarx; + int ret = + shm_td_iq_channel_rx(vrtsim_state->channel, vrtsim_state->last_received_sample, nsamps, global_dl_ant, samplesVoid[aarx]); + if (ret == CHANNEL_ERROR_TOO_LATE) { + vrtsim_state->rx_samples_late += nsamps; + } else if (ret == CHANNEL_ERROR_TOO_EARLY) { + vrtsim_state->rx_early += 1; + } + } } vrtsim_state->rx_samples_total += nsamps; *ptimestamp = vrtsim_state->last_received_sample; @@ -693,6 +1019,9 @@ static void vrtsim_end(openair0_device_t *device) shutdown_actor(&vrtsim_state->channel_modelling_actors[i]); } free(vrtsim_state->channel_modelling_actors); + if (vrtsim_state->use_cirdb && vrtsim_state->role == ROLE_SERVER && vrtsim_state->num_ues > 1) { + pthread_mutex_destroy(&vrtsim_state->cirdb_mutex); + } for (int i = 1; i < vrtsim_state->peer_info.num_rx_antennas; i++) { histogram_merge(&tx_timing->tx_histogram, &tx_timing[i].tx_histogram); tx_timing->tx_early += tx_timing[i].tx_early; @@ -708,6 +1037,10 @@ static void vrtsim_end(openair0_device_t *device) taps_client_stop(); } } + if (vrtsim_state->role == ROLE_SERVER && vrtsim_state->ul_combine_buffer) { + free(vrtsim_state->ul_combine_buffer); + vrtsim_state->ul_combine_buffer = NULL; + } shm_td_iq_channel_abort(vrtsim_state->channel); sleep(1); shm_td_iq_channel_destroy(vrtsim_state->channel); From 694f7bea084393f52255a95a2a9870375b901f96 Mon Sep 17 00:00:00 2001 From: Merkebu Girmay Date: Fri, 27 Feb 2026 22:10:08 -0800 Subject: [PATCH 2/2] Add CI test for multi-UE vrtsim with CIRDB --- .../container_5g_vrtsim_multiue_gh.xml | 183 +++++++++++++++ .../5g_vrtsim_multiue/docker-compose.yaml | 212 ++++++++++++++++++ 2 files changed, 395 insertions(+) create mode 100644 ci-scripts/xml_files/container_5g_vrtsim_multiue_gh.xml create mode 100644 ci-scripts/yaml_files/5g_vrtsim_multiue/docker-compose.yaml diff --git a/ci-scripts/xml_files/container_5g_vrtsim_multiue_gh.xml b/ci-scripts/xml_files/container_5g_vrtsim_multiue_gh.xml new file mode 100644 index 0000000000..c2da848374 --- /dev/null +++ b/ci-scripts/xml_files/container_5g_vrtsim_multiue_gh.xml @@ -0,0 +1,183 @@ + + + vrtsim-5gnr-multiue-gh + Multi-UE gNB vrtsim + CIRDB + wrench + + + Pull_Local_Registry + Pull Images from Local Registry + localhost + oai-gnb oai-nr-ue + arm_ + + + + Create_Workspace + Create new Workspace + localhost + + + + Deploy_Object + Deploy OAI 5G CoreNetwork + localhost + ci-scripts/yaml_files/5g_vrtsim_multiue + mysql oai-amf oai-smf oai-upf oai-ext-dn + + + + Deploy_Object + Deploy OAI 5G gNB+UE RF sim SA + localhost + ci-scripts/yaml_files/5g_vrtsim_multiue + oai-gnb oai-nr-ue0 oai-nr-ue1 + + + + Attach_UE + Attach OAI UE 1 (Wait for IP) + rfsim5g_ue + localhost + + + + Attach_UE + Attach OAI UE 2 (Wait for IP) + rfsim5g_ue2 + localhost + + + + Ping + Ping ext-dn from NR-UE + rfsim5g_ue + localhost + rfsim5g_ext_dn + localhost + -c 20 -i 0.25 + 5 + + + + Ping + Ping NR-UE from ext-dn + rfsim5g_ext_dn + localhost + rfsim5g_ue + localhost + -c 20 -i 0.25 + 5 + + + + Iperf + Iperf UE 1 (DL/3Mbps/UDP)(30 sec) + -u -b 3M -t 30 -R + rfsim5g_ue + localhost + rfsim5g_ext_dn + localhost + 5 + 90 + + + + Iperf + Iperf UE 1 (UL/1Mbps/UDP)(30 sec) + -u -b 1M -t 30 + rfsim5g_ue + localhost + rfsim5g_ext_dn + localhost + 5 + 90 + + + + Ping + Ping ext-dn from UE 2 + rfsim5g_ue2 + localhost + rfsim5g_ext_dn + localhost + -c 20 -i 0.25 + 5 + + + + Ping + Ping UE 2 from ext-dn + rfsim5g_ext_dn + localhost + rfsim5g_ue2 + localhost + -c 20 -i 0.25 + 5 + + + + Iperf + Iperf UE 2 (DL/3Mbps/UDP)(30 sec) + -u -b 3M -t 30 -R + rfsim5g_ue2 + localhost + rfsim5g_ext_dn + localhost + 5 + 90 + + + + Iperf + Iperf UE 2 (UL/1Mbps/UDP)(30 sec) + -u -b 1M -t 30 + rfsim5g_ue2 + localhost + rfsim5g_ext_dn + localhost + 5 + 90 + + + + Undeploy_Object + true + Undeploy all OAI 5G stack + localhost + ci-scripts/yaml_files/5g_vrtsim_multiue + + oai-gnb=EndsWithBye oai-nr-ue0 oai-nr-ue1 + + + + + Clean_Test_Server_Images + true + Clean Test Images on Test Server + localhost + oai-gnb oai-nr-ue + + + diff --git a/ci-scripts/yaml_files/5g_vrtsim_multiue/docker-compose.yaml b/ci-scripts/yaml_files/5g_vrtsim_multiue/docker-compose.yaml new file mode 100644 index 0000000000..c87be3a998 --- /dev/null +++ b/ci-scripts/yaml_files/5g_vrtsim_multiue/docker-compose.yaml @@ -0,0 +1,212 @@ +services: + # CIR Generator service - generates CIRDB files on every CI run + cir-generator: + image: oaisoftwarealliance/cir-generator:test + environment: + GENERATOR_ARGS: "--demo" + OUTPUT: "--out /opt/output/cir_db.bin" + volumes: + - cirdb_data:/opt/output + + mysql: + container_name: "rfsim5g-mysql" + image: mysql:8.0 + init: true + volumes: + - ../5g_rfsimulator/oai_db.sql:/docker-entrypoint-initdb.d/oai_db.sql + - ../5g_rfsimulator/mysql-healthcheck.sh:/tmp/mysql-healthcheck.sh + environment: + - TZ=Europe/Paris + - MYSQL_DATABASE=oai_db + - MYSQL_USER=test + - MYSQL_PASSWORD=test + - MYSQL_ROOT_PASSWORD=linux + healthcheck: + test: /bin/bash -c "/tmp/mysql-healthcheck.sh" + interval: 10s + timeout: 5s + start_period: 10s + start_interval: 500ms + retries: 30 + networks: + public_net: + ipv4_address: 192.168.71.131 + + oai-amf: + container_name: "rfsim5g-oai-amf" + image: oaisoftwarealliance/oai-amf:v2.1.10 + environment: + - TZ=Europe/paris + volumes: + - ../5g_rfsimulator/mini_nonrf_config.yaml:/openair-amf/etc/config.yaml + depends_on: + - mysql + networks: + public_net: + ipv4_address: 192.168.71.132 + + oai-smf: + container_name: "rfsim5g-oai-smf" + image: oaisoftwarealliance/oai-smf:v2.1.10 + environment: + - TZ=Europe/Paris + volumes: + - ../5g_rfsimulator/mini_nonrf_config.yaml:/openair-smf/etc/config.yaml + depends_on: + - oai-amf + networks: + public_net: + ipv4_address: 192.168.71.133 + + oai-upf: + container_name: "rfsim5g-oai-upf" + image: oaisoftwarealliance/oai-upf:v2.1.10 + init: true + environment: + - TZ=Europe/Paris + volumes: + - ../5g_rfsimulator/mini_nonrf_config.yaml:/openair-upf/etc/config.yaml + depends_on: + - oai-smf + cap_add: + - NET_ADMIN + - SYS_ADMIN + cap_drop: + - ALL + privileged: true + networks: + public_net: + ipv4_address: 192.168.71.134 + interface_name: eth0 + traffic_net: + ipv4_address: 192.168.72.134 + interface_name: eth1 + + oai-ext-dn: + privileged: true + container_name: rfsim5g-oai-ext-dn + image: oaisoftwarealliance/trf-gen-cn5g:latest + init: true + entrypoint: /bin/bash -c \ + "iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE;"\ + "ip route add 12.1.1.0/24 via 192.168.72.134 dev eth0; sleep infinity" + depends_on: + - oai-upf + networks: + traffic_net: + ipv4_address: 192.168.72.135 + healthcheck: + test: /bin/bash -c "ping -c 2 192.168.72.134" + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + start_interval: 500ms + + oai-gnb: + image: ${REGISTRY-oaisoftwarealliance/}${GNB_IMG:-oai-gnb}:${TAG:-develop} + container_name: rfsim5g-oai-gnb + cap_drop: + - ALL + environment: + USE_ADDITIONAL_OPTIONS: -E --log_config.global_log_options level,nocolor,time --device.name vrtsim --vrtsim.role server --vrtsim.num_ues 2 --vrtsim.cirdb 1 --vrtsim.cirdb_file /cirdb/cir_db.bin --vrtsim.cirdb_yaml /cirdb/cir_db.yaml --vrtsim.ue_config.[0].antennas 1x2 --vrtsim.ue_config.[0].model_id 0 --vrtsim.ue_config.[0].ds_ns 10.0 --vrtsim.ue_config.[0].speed_mps 1.5 --vrtsim.ue_config.[1].antennas 2x2 --vrtsim.ue_config.[1].model_id 1 --vrtsim.ue_config.[1].ds_ns 10.0 --vrtsim.ue_config.[1].speed_mps 1.5 --gNBs.[0].min_rxtxtime 8 + depends_on: + cir-generator: + condition: service_completed_successfully + oai-ext-dn: + condition: service_started + networks: + public_net: + ipv4_address: 192.168.71.140 + volumes: + - ../../conf_files/gnb.sa.band78.106prb.vrtsim.2x2.yaml:/opt/oai-gnb/etc/gnb.yaml + - cirdb_data:/cirdb:ro + - tmp_data:/tmp/ + healthcheck: + test: /bin/bash -c "pgrep nr-softmodem" + interval: 10s + timeout: 5s + start_period: 10s + start_interval: 500ms + retries: 5 + ipc: host + + oai-nr-ue0: + image: ${REGISTRY-oaisoftwarealliance/}${NRUE_IMG:-oai-nr-ue}:${TAG:-develop} + container_name: rfsim5g-oai-nr-ue + cap_drop: + - ALL + cap_add: + - NET_ADMIN + - NET_RAW + environment: + USE_ADDITIONAL_OPTIONS: -E -r 106 --numerology 1 --band 78 -C 3319680000 --ssb 516 --uicc0.imsi 208990100001100 --log_config.global_log_options level,nocolor,time --device.name vrtsim --vrtsim.role client --vrtsim.ue_id 0 --ue-nb-ant-tx 1 --ue-nb-ant-rx 2 + depends_on: + - oai-gnb + networks: + public_net: + ipv4_address: 192.168.71.150 + devices: + - /dev/net/tun:/dev/net/tun + volumes: + - ../../conf_files/nrue.vrtsim.chanmod.yaml:/opt/oai-nr-ue/etc/nr-ue.yaml + - tmp_data:/tmp/ + healthcheck: + test: /bin/bash -c "pgrep nr-uesoftmodem" + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + start_interval: 500ms + ipc: host + + oai-nr-ue1: + image: ${REGISTRY-oaisoftwarealliance/}${NRUE_IMG:-oai-nr-ue}:${TAG:-develop} + container_name: rfsim5g-oai-nr-ue2 + cap_drop: + - ALL + cap_add: + - NET_ADMIN + - NET_RAW + environment: + USE_ADDITIONAL_OPTIONS: -E -r 106 --numerology 1 --band 78 -C 3319680000 --ssb 516 --uicc0.imsi 208990100001101 --log_config.global_log_options level,nocolor,time --device.name vrtsim --vrtsim.role client --vrtsim.ue_id 1 --ue-nb-ant-tx 2 --ue-nb-ant-rx 2 + depends_on: + - oai-gnb + networks: + public_net: + ipv4_address: 192.168.71.151 + devices: + - /dev/net/tun:/dev/net/tun + volumes: + - ../../conf_files/nrue.vrtsim.chanmod.yaml:/opt/oai-nr-ue/etc/nr-ue.yaml + - tmp_data:/tmp/ + healthcheck: + test: /bin/bash -c "pgrep nr-uesoftmodem" + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + start_interval: 500ms + ipc: host + +networks: + public_net: + driver: bridge + name: rfsim5g-oai-public-net + ipam: + config: + - subnet: 192.168.71.128/26 + driver_opts: + com.docker.network.bridge.name: "rfsim5g-public" + traffic_net: + driver: bridge + name: rfsim5g-oai-traffic-net + ipam: + config: + - subnet: 192.168.72.128/26 + driver_opts: + com.docker.network.bridge.name: "rfsim5g-traffic" + +volumes: + tmp_data: + cirdb_data: