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:
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);