Commit 888d4a91 authored by Jaroslava Fiedlerova's avatar Jaroslava Fiedlerova

Merge remote-tracking branch 'origin/vrtsim-multi-ue' into integration_2026_w11 (!3904)

add multi-ue support to vrtsim

Summary
Extending vrtsim to support multiple UEs with per-UE channel emulation using
CIRDB. Each UE can have different antenna configurations and independent
channel conditions (TDL models, delay spreads, and mobility speeds). This
includes:
- Add num_ues and ue_id parameters for server and client configuration
- Implement antenna offset multiplexing for UL combining and DL distribution
- Support heterogeneous antenna configurations across UEs (e.g., UE0: 1x2,
  UE1: 2x2)
- Integrate CIRDB for per-UE channel modeling with independent TDL models,
  delay spreads, and speeds
- Replace comma-separated per-UE params with structured ue_config blocks for
  cleaner configuration

Usage:
* Start 5G core network
    cd openairinterface5g/doc/tutorial_resources/oai-cn5g
    docker-compose up -d

* Create network namespaces
    sudo ip netns add ue0
    sudo ip netns add ue1

* Start gNB (2x2 with multi-UE CIRDB)
    sudo ./nr-softmodem -O ../../ci-scripts/conf_files/gnb.sa.band78.106prb.vrtsim.2x2.yaml --device.name vrtsim --vrtsim.role server --vrtsim.num_ues 2 --vrtsim.cirdb 1 --vrtsim.cirdb_file ~/raytracing-channel-emulator/server/external_taps/cir_db.bin --vrtsim.cirdb_yaml ~/raytracing-channel-emulator/server/external_taps/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 30.0 --vrtsim.ue_config.[1].speed_mps 30.0

* Start UE 0 (1x2 antennas, TDL-A, 10ns DS, 1.5 m/s)
    sudo ip netns exec ue0 ./nr-uesoftmodem -O ../../ci-scripts/conf_files/nrue.vrtsim.chanmod.yaml -C 3319680000 -r 106 --numerology 1 --ssb 516 --device.name vrtsim --vrtsim.role client --vrtsim.ue_id 0 --band 78 --ue-nb-ant-tx 1 --ue-nb-ant-rx 2 --uicc0.imsi 001010000000001

* Start UE 1 (2x2 antennas, TDL-B, 30ns DS, 30 m/s)
    sudo ip netns exec ue1 ./nr-uesoftmodem -O ../../ci-scripts/conf_files/nrue.vrtsim.chanmod.yaml -C 3319680000 -r 106 --numerology 1 --ssb 516 --device.name vrtsim --vrtsim.role client --vrtsim.ue_id 1 --band 78 --ue-nb-ant-tx 2 --ue-nb-ant-rx 2 --uicc0.imsi 001010000000002
parents 00394d78 694f7bea
<!--
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
-->
<testCaseList>
<htmlTabRef>vrtsim-5gnr-multiue-gh</htmlTabRef>
<htmlTabName>Multi-UE gNB vrtsim + CIRDB</htmlTabName>
<htmlTabIcon>wrench</htmlTabIcon>
<testCase>
<class>Pull_Local_Registry</class>
<desc>Pull Images from Local Registry</desc>
<node>localhost</node>
<images>oai-gnb oai-nr-ue</images>
<tag_prefix>arm_</tag_prefix>
</testCase>
<testCase>
<class>Create_Workspace</class>
<desc>Create new Workspace</desc>
<node>localhost</node>
</testCase>
<testCase>
<class>Deploy_Object</class>
<desc>Deploy OAI 5G CoreNetwork</desc>
<node>localhost</node>
<yaml_path>ci-scripts/yaml_files/5g_vrtsim_multiue</yaml_path>
<services>mysql oai-amf oai-smf oai-upf oai-ext-dn</services>
</testCase>
<testCase>
<class>Deploy_Object</class>
<desc>Deploy OAI 5G gNB+UE RF sim SA</desc>
<node>localhost</node>
<yaml_path>ci-scripts/yaml_files/5g_vrtsim_multiue</yaml_path>
<services>oai-gnb oai-nr-ue0 oai-nr-ue1</services>
</testCase>
<testCase>
<class>Attach_UE</class>
<desc>Attach OAI UE 1 (Wait for IP)</desc>
<id>rfsim5g_ue</id>
<node>localhost</node>
</testCase>
<testCase>
<class>Attach_UE</class>
<desc>Attach OAI UE 2 (Wait for IP)</desc>
<id>rfsim5g_ue2</id>
<node>localhost</node>
</testCase>
<testCase>
<class>Ping</class>
<desc>Ping ext-dn from NR-UE</desc>
<id>rfsim5g_ue</id>
<node>localhost</node>
<svr_id>rfsim5g_ext_dn</svr_id>
<svr_node>localhost</svr_node>
<ping_args>-c 20 -i 0.25</ping_args>
<ping_packetloss_threshold>5</ping_packetloss_threshold>
</testCase>
<testCase>
<class>Ping</class>
<desc>Ping NR-UE from ext-dn</desc>
<id>rfsim5g_ext_dn</id>
<node>localhost</node>
<svr_id>rfsim5g_ue</svr_id>
<svr_node>localhost</svr_node>
<ping_args>-c 20 -i 0.25</ping_args>
<ping_packetloss_threshold>5</ping_packetloss_threshold>
</testCase>
<testCase>
<class>Iperf</class>
<desc>Iperf UE 1 (DL/3Mbps/UDP)(30 sec)</desc>
<iperf_args>-u -b 3M -t 30 -R</iperf_args>
<id>rfsim5g_ue</id>
<node>localhost</node>
<svr_id>rfsim5g_ext_dn</svr_id>
<svr_node>localhost</svr_node>
<iperf_packetloss_threshold>5</iperf_packetloss_threshold>
<iperf_bitrate_threshold>90</iperf_bitrate_threshold>
</testCase>
<testCase>
<class>Iperf</class>
<desc>Iperf UE 1 (UL/1Mbps/UDP)(30 sec)</desc>
<iperf_args>-u -b 1M -t 30</iperf_args>
<id>rfsim5g_ue</id>
<node>localhost</node>
<svr_id>rfsim5g_ext_dn</svr_id>
<svr_node>localhost</svr_node>
<iperf_packetloss_threshold>5</iperf_packetloss_threshold>
<iperf_bitrate_threshold>90</iperf_bitrate_threshold>
</testCase>
<testCase>
<class>Ping</class>
<desc>Ping ext-dn from UE 2</desc>
<id>rfsim5g_ue2</id>
<node>localhost</node>
<svr_id>rfsim5g_ext_dn</svr_id>
<svr_node>localhost</svr_node>
<ping_args>-c 20 -i 0.25</ping_args>
<ping_packetloss_threshold>5</ping_packetloss_threshold>
</testCase>
<testCase>
<class>Ping</class>
<desc>Ping UE 2 from ext-dn</desc>
<id>rfsim5g_ext_dn</id>
<node>localhost</node>
<svr_id>rfsim5g_ue2</svr_id>
<svr_node>localhost</svr_node>
<ping_args>-c 20 -i 0.25</ping_args>
<ping_packetloss_threshold>5</ping_packetloss_threshold>
</testCase>
<testCase>
<class>Iperf</class>
<desc>Iperf UE 2 (DL/3Mbps/UDP)(30 sec)</desc>
<iperf_args>-u -b 3M -t 30 -R</iperf_args>
<id>rfsim5g_ue2</id>
<node>localhost</node>
<svr_id>rfsim5g_ext_dn</svr_id>
<svr_node>localhost</svr_node>
<iperf_packetloss_threshold>5</iperf_packetloss_threshold>
<iperf_bitrate_threshold>90</iperf_bitrate_threshold>
</testCase>
<testCase>
<class>Iperf</class>
<desc>Iperf UE 2 (UL/1Mbps/UDP)(30 sec)</desc>
<iperf_args>-u -b 1M -t 30</iperf_args>
<id>rfsim5g_ue2</id>
<node>localhost</node>
<svr_id>rfsim5g_ext_dn</svr_id>
<svr_node>localhost</svr_node>
<iperf_packetloss_threshold>5</iperf_packetloss_threshold>
<iperf_bitrate_threshold>90</iperf_bitrate_threshold>
</testCase>
<testCase>
<class>Undeploy_Object</class>
<always_exec>true</always_exec>
<desc>Undeploy all OAI 5G stack</desc>
<node>localhost</node>
<yaml_path>ci-scripts/yaml_files/5g_vrtsim_multiue</yaml_path>
<analysis>
<services>oai-gnb=EndsWithBye oai-nr-ue0 oai-nr-ue1</services>
</analysis>
</testCase>
<testCase>
<class>Clean_Test_Server_Images</class>
<always_exec>true</always_exec>
<desc>Clean Test Images on Test Server</desc>
<node>localhost</node>
<images>oai-gnb oai-nr-ue</images>
</testCase>
</testCaseList>
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:
......@@ -44,6 +44,7 @@
#include <common/utils/load_module_shlib.h>
#include <common/utils/telnetsrv/telnetsrv.h>
#include <common/config/config_userapi.h>
#include <common/openairinterface5g_limits.h>
#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);
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 = {
.server_num_rx_antennas = device->openair0_cfg[0].rx_num_channels,
.client_num_rx_antennas = vrtsim_state->client_num_rx_antennas,
.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,
};
server_publish_client_info(client_info, vrtsim_state->connection_descriptor);
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);
......
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