diff --git a/ci-scripts/xml_files/container_5g_fdd_rfsim.xml b/ci-scripts/xml_files/container_5g_fdd_rfsim.xml
index 276bb5b9a3..1c43120131 100644
--- a/ci-scripts/xml_files/container_5g_fdd_rfsim.xml
+++ b/ci-scripts/xml_files/container_5g_fdd_rfsim.xml
@@ -43,7 +43,7 @@
Pull_Local_Registry
Pull Images from Local Registry
0
- oai-gnb-asan oai-nr-ue-asan
+ oai-gnb oai-nr-ue
Create_Workspace
@@ -134,7 +134,7 @@
true
Clean Test Images on Test Server
0
- oai-gnb-asan oai-nr-ue-asan
+ oai-gnb oai-nr-ue
diff --git a/ci-scripts/yaml_files/5g_fdd_rfsimulator/docker-compose.yaml b/ci-scripts/yaml_files/5g_fdd_rfsimulator/docker-compose.yaml
index a6103a3815..8c4688262b 100644
--- a/ci-scripts/yaml_files/5g_fdd_rfsimulator/docker-compose.yaml
+++ b/ci-scripts/yaml_files/5g_fdd_rfsimulator/docker-compose.yaml
@@ -86,8 +86,7 @@ services:
cap_drop:
- ALL
environment:
- USE_ADDITIONAL_OPTIONS: -E --rfsim --log_config.global_log_options level,nocolor,time
- ASAN_OPTIONS: detect_leaks=0
+ USE_ADDITIONAL_OPTIONS: -E --log_config.global_log_options level,nocolor,time --device.name vrtsim --vrtsim.role server --vrtsim.timescale 0.5
depends_on:
- oai-ext-dn
networks:
@@ -95,11 +94,13 @@ services:
ipv4_address: 192.168.71.140
volumes:
- ../../conf_files/gnb.sa.band66.106prb.rfsim.conf:/opt/oai-gnb/etc/gnb.conf
+ - tmp_data:/tmp/
healthcheck:
test: /bin/bash -c "pgrep nr-softmodem"
interval: 10s
timeout: 5s
retries: 5
+ ipc: host
oai-nr-ue:
image: ${REGISTRY:-oaisoftwarealliance}/${NRUE_IMG:-oai-nr-ue}:${TAG:-develop}
container_name: rfsim5g-oai-nr-ue
@@ -109,7 +110,7 @@ services:
- NET_ADMIN # for interface bringup
- NET_RAW # for ping
environment:
- USE_ADDITIONAL_OPTIONS: -E --rfsim -r 106 --numerology 1 --uicc0.imsi 208990100001100 --band 66 -C 2169090000 --CO -400000000 --ssb 378 --rfsimulator.serveraddr 192.168.71.140 --log_config.global_log_options level,nocolor,time
+ USE_ADDITIONAL_OPTIONS: -E -r 106 --numerology 1 --uicc0.imsi 208990100001100 --band 66 -C 2169090000 --CO -400000000 --ssb 378 --log_config.global_log_options level,nocolor,time --device.name vrtsim
depends_on:
- oai-gnb
networks:
@@ -119,11 +120,13 @@ services:
- /dev/net/tun:/dev/net/tun
volumes:
- ../../conf_files/nrue.uicc.conf:/opt/oai-nr-ue/etc/nr-ue.conf
+ - tmp_data:/tmp/
healthcheck:
test: /bin/bash -c "pgrep nr-uesoftmodem"
interval: 10s
timeout: 5s
retries: 5
+ ipc: host
networks:
public_net:
@@ -142,3 +145,6 @@ networks:
- subnet: 192.168.72.128/26
driver_opts:
com.docker.network.bridge.name: "rfsim5g-traffic"
+
+volumes:
+ tmp_data:
diff --git a/cmake_targets/build_oai b/cmake_targets/build_oai
index 27bb63ff89..f0d08328cf 100755
--- a/cmake_targets/build_oai
+++ b/cmake_targets/build_oai
@@ -485,7 +485,7 @@ function main() {
# add some default libraries that should always be built
# for eNB, gNB, UEs, simulators
if [[ $gNB == 1 || $eNB == 1 || $UE == 1 || $nrUE == 1 || $SIMUS_PHY == 1 || $RU == 1 ]]; then
- TARGET_LIST="$TARGET_LIST params_libconfig coding rfsimulator dfts params_yaml"
+ TARGET_LIST="$TARGET_LIST params_libconfig coding rfsimulator dfts params_yaml vrtsim"
fi
mkdir -p $DIR/$BUILD_DIR/build
diff --git a/common/utils/CMakeLists.txt b/common/utils/CMakeLists.txt
index ccb15bfd47..b311d0542f 100644
--- a/common/utils/CMakeLists.txt
+++ b/common/utils/CMakeLists.txt
@@ -21,3 +21,4 @@ if (ENABLE_TESTS)
endif()
add_subdirectory(barrier)
add_subdirectory(actor)
+add_subdirectory(shm_iq_channel)
diff --git a/common/utils/actor/CMakeLists.txt b/common/utils/actor/CMakeLists.txt
index 40057d347e..182f54651d 100644
--- a/common/utils/actor/CMakeLists.txt
+++ b/common/utils/actor/CMakeLists.txt
@@ -1,6 +1,6 @@
add_library(actor actor.c)
-target_include_directories(actor PUBLIC ./)
-target_link_libraries(actor PUBLIC thread-pool)
+# Only include header files due to issues with ODR in threadPool binaries
+target_include_directories(actor PUBLIC ./ ../threadPool/)
if (ENABLE_TESTS)
add_subdirectory(tests)
endif()
diff --git a/common/utils/shm_iq_channel/CMakeLists.txt b/common/utils/shm_iq_channel/CMakeLists.txt
new file mode 100644
index 0000000000..57c7d343ef
--- /dev/null
+++ b/common/utils/shm_iq_channel/CMakeLists.txt
@@ -0,0 +1,6 @@
+add_library(shm_td_iq_channel shm_td_iq_channel.c)
+target_include_directories(shm_td_iq_channel PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
+
+if (ENABLE_TESTS)
+ add_subdirectory(tests)
+endif()
diff --git a/common/utils/shm_iq_channel/shm_td_iq_channel.c b/common/utils/shm_iq_channel/shm_td_iq_channel.c
new file mode 100644
index 0000000000..89e09ddb1f
--- /dev/null
+++ b/common/utils/shm_iq_channel/shm_td_iq_channel.c
@@ -0,0 +1,286 @@
+/*
+ * Licensed to the OpenAirInterface (OAI) Software Alliance under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The OpenAirInterface Software Alliance licenses this file to You under
+ * the OAI Public License, Version 1.1 (the "License"); you may not use this file
+ * except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.openairinterface.org/?page_id=698
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *-------------------------------------------------------------------------------
+ * For more information about the OpenAirInterface (OAI) Software Alliance:
+ * contact@openairinterface.org
+ */
+
+#include "shm_td_iq_channel.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "assertions.h"
+#include "utils.h"
+#include "common/utils/threadPool/pthread_utils.h"
+
+#define CIRCULAR_BUFFER_SIZE (30720 * 14 * 20)
+
+typedef struct {
+ int magic;
+ int num_antennas_tx;
+ int num_antennas_rx;
+ uint64_t timestamp;
+ bool is_connected;
+ pthread_mutex_t mutex;
+ pthread_cond_t cond;
+} ShmTDIQChannelData;
+
+typedef struct ShmTDIQChannel_s {
+ IQChannelType type;
+ ShmTDIQChannelData *data;
+ uint64_t last_timestamp;
+ char name[256];
+ sample_t *tx_iq_data;
+ sample_t *rx_iq_data;
+} ShmTDIQChannel;
+
+ShmTDIQChannel *shm_td_iq_channel_create(const char *name, int num_tx_ant, int num_rx_ant)
+{
+ // Create shared memory segment
+ int fd = shm_open(name, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR);
+ AssertFatal(fd != -1, "shm_open failed: %s\n", strerror(errno));
+ size_t tx_buffer_size = CIRCULAR_BUFFER_SIZE * sizeof(sample_t) * num_tx_ant;
+ size_t rx_buffer_size = CIRCULAR_BUFFER_SIZE * sizeof(sample_t) * num_rx_ant;
+ size_t total_size = sizeof(ShmTDIQChannelData) + tx_buffer_size + rx_buffer_size;
+
+ // Set the size of the shared memory segment
+ int res = ftruncate(fd, total_size);
+ AssertFatal(res != -1, "ftruncate failed: %s\n", strerror(errno));
+
+ // Map shared memory segment to address space
+ ShmTDIQChannelData *shm_ptr = mmap(NULL, total_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+ AssertFatal(shm_ptr != MAP_FAILED, "mmap failed: %s\n", strerror(errno));
+
+ // Initialize shared memory
+ memset(shm_ptr, 0, total_size);
+ shm_ptr->num_antennas_tx = num_tx_ant;
+ shm_ptr->num_antennas_rx = num_rx_ant;
+ shm_ptr->is_connected = false;
+ ShmTDIQChannel *channel = calloc_or_fail(1, sizeof(ShmTDIQChannel));
+ strncpy(channel->name, name, sizeof(channel->name) - 1);
+ channel->tx_iq_data = (sample_t *)(shm_ptr + 1);
+ channel->rx_iq_data = channel->tx_iq_data + tx_buffer_size / sizeof(sample_t);
+ channel->data = shm_ptr;
+ channel->type = IQ_CHANNEL_TYPE_SERVER;
+ channel->last_timestamp = 0;
+ pthread_mutexattr_t mutex_attr;
+ pthread_condattr_t cond_attr;
+ int ret = pthread_mutexattr_init(&mutex_attr);
+ AssertFatal(ret == 0, "pthread_mutexattr_init() failed: errno %d, %s\n", errno, strerror(errno));
+
+ ret = pthread_condattr_init(&cond_attr);
+ AssertFatal(ret == 0, "pthread_condattr_init() failed: errno %d, %s\n", errno, strerror(errno));
+
+ ret = pthread_mutexattr_setpshared(&mutex_attr, PTHREAD_PROCESS_SHARED);
+ AssertFatal(ret == 0, "pthread_mutexattr_setpshared() failed: errno %d, %s\n", errno, strerror(errno));
+
+ ret = pthread_condattr_setpshared(&cond_attr, PTHREAD_PROCESS_SHARED);
+ AssertFatal(ret == 0, "pthread_condattr_setpshared() failed: errno %d, %s\n", errno, strerror(errno));
+
+ ret = pthread_mutex_init(&shm_ptr->mutex, &mutex_attr);
+ AssertFatal(ret == 0, "pthread_mutex_init() failed: errno %d, %s\n", errno, strerror(errno));
+
+ ret = pthread_cond_init(&shm_ptr->cond, &cond_attr);
+ AssertFatal(ret == 0, "pthread_cond_init() failed: errno %d, %s\n", errno, strerror(errno));
+
+ shm_ptr->magic = SHM_MAGIC_NUMBER;
+ close(fd);
+ return channel;
+}
+
+ShmTDIQChannel *shm_td_iq_channel_connect(const char *name, int timeout_in_seconds)
+{
+ // Create shared memory segment
+ int fd = -1;
+ while (timeout_in_seconds > 0 && fd == -1) {
+ fd = shm_open(name, O_RDWR, S_IRUSR | S_IWUSR);
+ timeout_in_seconds--;
+ printf("Waiting for server to create shared memory segment\n");
+ sleep(1);
+ }
+ AssertFatal(fd != -1, "shm_open() failed: errno %d, %s", errno, strerror(errno));
+
+ struct stat buf;
+ fstat(fd, &buf);
+ size_t total_size = buf.st_size;
+
+ // Map shared memory segment to address space
+ ShmTDIQChannelData *shm_ptr = mmap(NULL, total_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+ if (shm_ptr == MAP_FAILED) {
+ perror("mmap");
+ exit(1);
+ }
+
+ ShmTDIQChannel *channel = calloc_or_fail(1, sizeof(ShmTDIQChannel));
+ channel->data = shm_ptr;
+ channel->tx_iq_data = (sample_t *)(shm_ptr + 1);
+ size_t tx_buffer_size = CIRCULAR_BUFFER_SIZE * sizeof(sample_t) * channel->data->num_antennas_tx;
+ channel->rx_iq_data = channel->tx_iq_data + tx_buffer_size / sizeof(sample_t);
+ channel->type = IQ_CHANNEL_TYPE_CLIENT;
+ channel->last_timestamp = 0;
+ while (shm_ptr->magic != SHM_MAGIC_NUMBER) {
+ printf("Waiting for server to initialize shared memory\n");
+ sleep(1);
+ }
+ shm_ptr->is_connected = true;
+ close(fd);
+ return channel;
+}
+
+IQChannelErrorType shm_td_iq_channel_tx(ShmTDIQChannel *channel,
+ uint64_t timestamp,
+ uint64_t num_samples,
+ int antenna,
+ const sample_t *tx_iq_data)
+{
+ ShmTDIQChannelData *data = channel->data;
+ if (data->is_connected == false) {
+ return CHANNEL_ERROR_NOT_CONNECTED;
+ }
+ // timestamp in the past
+ uint64_t current_time = data->timestamp;
+ if (timestamp < current_time) {
+ return CHANNEL_ERROR_TOO_LATE;
+ }
+ // timestamp is too far in the future
+ if (timestamp - current_time + num_samples >= CIRCULAR_BUFFER_SIZE) {
+ return CHANNEL_ERROR_TOO_EARLY;
+ }
+
+ sample_t *base_ptr;
+ if (channel->type == IQ_CHANNEL_TYPE_CLIENT) {
+ base_ptr = channel->rx_iq_data + antenna * CIRCULAR_BUFFER_SIZE;
+ } else {
+ base_ptr = channel->tx_iq_data + antenna * CIRCULAR_BUFFER_SIZE;
+ }
+
+ uint64_t first_sample = timestamp % CIRCULAR_BUFFER_SIZE;
+ uint64_t last_sample = first_sample + num_samples - 1;
+ if (last_sample >= CIRCULAR_BUFFER_SIZE) {
+ size_t num_samples_first_copy = CIRCULAR_BUFFER_SIZE - first_sample;
+ memcpy(base_ptr + first_sample, tx_iq_data, num_samples_first_copy * sizeof(sample_t));
+ memcpy(base_ptr, tx_iq_data + num_samples_first_copy, (num_samples - num_samples_first_copy) * sizeof(sample_t));
+ } else {
+ memcpy(base_ptr + first_sample, tx_iq_data, num_samples * sizeof(sample_t));
+ }
+ return CHANNEL_NO_ERROR;
+}
+
+IQChannelErrorType shm_td_iq_channel_rx(ShmTDIQChannel *channel,
+ uint64_t timestamp,
+ uint64_t num_samples,
+ int antenna,
+ sample_t *tx_iq_data)
+{
+ ShmTDIQChannelData *data = channel->data;
+ if (data->is_connected == false) {
+ return CHANNEL_ERROR_NOT_CONNECTED;
+ }
+ // timestamp in the future
+ uint64_t current_time = data->timestamp;
+ if (timestamp > current_time) {
+ return CHANNEL_ERROR_TOO_EARLY;
+ }
+ // timestamp is too far in the past
+ if (current_time - timestamp >= CIRCULAR_BUFFER_SIZE) {
+ return CHANNEL_ERROR_TOO_LATE;
+ }
+
+ sample_t *base_ptr;
+ if (channel->type == IQ_CHANNEL_TYPE_CLIENT) {
+ base_ptr = channel->tx_iq_data + antenna * CIRCULAR_BUFFER_SIZE;
+ } else {
+ base_ptr = channel->rx_iq_data + antenna * CIRCULAR_BUFFER_SIZE;
+ }
+
+ uint64_t first_sample = timestamp % CIRCULAR_BUFFER_SIZE;
+ uint64_t last_sample = first_sample + num_samples - 1;
+ if (last_sample >= CIRCULAR_BUFFER_SIZE) {
+ size_t num_samples_first_copy = CIRCULAR_BUFFER_SIZE - first_sample;
+ memcpy(tx_iq_data, base_ptr + first_sample, num_samples_first_copy * sizeof(sample_t));
+ memcpy(tx_iq_data + num_samples_first_copy, base_ptr, (num_samples - num_samples_first_copy) * sizeof(sample_t));
+ } else {
+ memcpy(tx_iq_data, base_ptr + first_sample, num_samples * sizeof(sample_t));
+ }
+ return CHANNEL_NO_ERROR;
+}
+
+void shm_td_iq_channel_produce_samples(ShmTDIQChannel *channel, size_t num_samples)
+{
+ ShmTDIQChannelData *data = channel->data;
+ if (channel->type != IQ_CHANNEL_TYPE_SERVER) {
+ return;
+ }
+ if (data->is_connected == false) {
+ return;
+ }
+ mutexlock(data->mutex);
+ data->timestamp += num_samples;
+ condbroadcast(data->cond);
+ mutexunlock(data->mutex);
+}
+
+void shm_td_iq_channel_wait(ShmTDIQChannel *channel, uint64_t timestamp)
+{
+ ShmTDIQChannelData *data = channel->data;
+ if (data->is_connected == false) {
+ abort();
+ return;
+ }
+
+ size_t current_timestamp = data->timestamp;
+ if (current_timestamp >= timestamp) {
+ return;
+ }
+ mutexlock(data->mutex);
+ while (current_timestamp < timestamp) {
+ condwait(data->cond, data->mutex);
+ current_timestamp = data->timestamp;
+ }
+ mutexunlock(data->mutex);
+ return;
+}
+
+uint64_t shm_td_iq_channel_get_current_sample(const ShmTDIQChannel *channel)
+{
+ ShmTDIQChannelData *data = channel->data;
+ return data->timestamp;
+}
+
+bool shm_td_iq_channel_is_connected(const ShmTDIQChannel *channel)
+{
+ return channel->data->is_connected;
+}
+
+void shm_td_iq_channel_destroy(ShmTDIQChannel *channel)
+{
+ ShmTDIQChannelData *data = channel->data;
+ size_t tx_buffer_size = CIRCULAR_BUFFER_SIZE * sizeof(sample_t) * data->num_antennas_tx;
+ size_t rx_buffer_size = CIRCULAR_BUFFER_SIZE * sizeof(sample_t) * data->num_antennas_rx;
+ size_t total_size = sizeof(ShmTDIQChannelData) + tx_buffer_size + rx_buffer_size;
+ munmap(data, total_size);
+ if (channel->type == IQ_CHANNEL_TYPE_SERVER) {
+ shm_unlink(channel->name);
+ }
+ free(channel);
+}
diff --git a/common/utils/shm_iq_channel/shm_td_iq_channel.h b/common/utils/shm_iq_channel/shm_td_iq_channel.h
new file mode 100644
index 0000000000..7c5a730914
--- /dev/null
+++ b/common/utils/shm_iq_channel/shm_td_iq_channel.h
@@ -0,0 +1,148 @@
+/*
+ * Licensed to the OpenAirInterface (OAI) Software Alliance under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The OpenAirInterface Software Alliance licenses this file to You under
+ * the OAI Public License, Version 1.1 (the "License"); you may not use this file
+ * except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.openairinterface.org/?page_id=698
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *-------------------------------------------------------------------------------
+ * For more information about the OpenAirInterface (OAI) Software Alliance:
+ * contact@openairinterface.org
+ */
+#ifndef SHM_IQ_CHANNEL_H
+#define SHM_IQ_CHANNEL_H
+
+#include "../threadPool/pthread_utils.h"
+#include
+#include
+#include
+
+#define SHM_MAGIC_NUMBER 0x12345678
+
+/**
+ * ShmTdIqChannel is a shared memory bidirectional time domain IQ channel with a single clock
+ * source. The server (clock source) shall create the channel while the client should connect to
+ * it.
+ *
+ * To write samples, send them via a pointer passed to shm_td_iq_channel_tx.
+ * To read samples, receive them via a pointer passed to shm_td_iq_channel_rx. You can either wait until
+ * the sample are available using shm_td_iq_channel_wait or poll using shm_td_iq_channel_rx until the return
+ * value is not CHANNEL_ERROR_TOO_EARLY.
+ * To indicate that samples are ready to be read by the client, call shm_td_iq_channel_produce_samples
+ * (server only).
+ *
+ * The timestamps used in the API are in absolute samples from the time the client/server connection started.
+ */
+
+typedef enum IQChannelType { IQ_CHANNEL_TYPE_SERVER, IQ_CHANNEL_TYPE_CLIENT } IQChannelType;
+typedef uint32_t sample_t;
+typedef enum IQChannelErrorType {
+ CHANNEL_NO_ERROR,
+ CHANNEL_ERROR_TOO_LATE,
+ CHANNEL_ERROR_TOO_EARLY,
+ CHANNEL_ERROR_NOT_CONNECTED
+} IQChannelErrorType;
+
+typedef struct ShmTDIQChannel_s ShmTDIQChannel;
+
+/**
+ * @brief Creates a shared memory IQ channel.
+ *
+ * @param name The name of the shared memory segment.
+ * @param num_tx_ant The number of TX antennas.
+ * @param num_rx_ant The number of RX antennas.
+ * @return A pointer to the created ShmTDIQChannel structure.
+ */
+ShmTDIQChannel *shm_td_iq_channel_create(const char *name, int num_tx_ant, int num_rx_ant);
+
+/**
+ * @brief Connects to an existing shared memory IQ channel.
+ *
+ * @param name The name of the shared memory segment.
+ * @param timeout_in_seconds The timeout in seconds for the connection attempt.
+ * @return A pointer to the connected ShmTDIQChannel structure.
+ */
+ShmTDIQChannel *shm_td_iq_channel_connect(const char *name, int timeout_in_seconds);
+
+/**
+ * @brief Transmit data in the channel
+ *
+ * @param channel The ShmTDIQChannel structure.
+ * @param timestamp The timestamp for which to get the TX IQ data slot.
+ * @param num_samples The number of samples to write.
+ * @param antenna The antenna index.
+ * @param tx_iq_data The TX IQ data to write.
+ *
+ * @return CHANNEL_NO_ERROR if successful, error type otherwise
+ */
+IQChannelErrorType shm_td_iq_channel_tx(ShmTDIQChannel *channel,
+ uint64_t timestamp,
+ uint64_t num_samples,
+ int antenna,
+ const sample_t *tx_iq_data);
+
+/**
+ * @brief Receive iq data from the channel
+ *
+ * @param channel The ShmTDIQChannel structure.
+ * @param timestamp The timestamp for which to get the RX IQ data slot.
+ * @param num_samples The number of samples to read.
+ * @param antenna The antenna index.
+ * @param tx_iq_data pointer to the RX IQ data slot.
+ * @return CHANNEL_NO_ERROR if successful, error type otherwise
+ */
+IQChannelErrorType shm_td_iq_channel_rx(ShmTDIQChannel *channel,
+ uint64_t timestamp,
+ uint64_t num_samples,
+ int antenna,
+ sample_t *tx_iq_data);
+
+/**
+ * @brief Advances the time in the channel by specified number of samples
+ *
+ * @param channel The ShmTDIQChannel structure.
+ * @param num_symbols The number of samples to produce.
+ */
+void shm_td_iq_channel_produce_samples(ShmTDIQChannel *channel, uint64_t num_samples);
+
+/**
+ * @brief Wait until sample at the specified timestamp is available
+ *
+ * @param channel The ShmTDIQChannel structure.
+ */
+void shm_td_iq_channel_wait(ShmTDIQChannel *channel, uint64_t timestamp);
+
+/**
+ * @brief Checks if the IQ channel is connected.
+ *
+ * @param channel The ShmTDIQChannel structure.
+ * @return True if the channel is connected, false otherwise.
+ */
+bool shm_td_iq_channel_is_connected(const ShmTDIQChannel *channel);
+
+/**
+ * @brief Destroys the shared memory IQ channel.
+ *
+ * @param channel The ShmTDIQChannel structure.
+ */
+void shm_td_iq_channel_destroy(ShmTDIQChannel *channel);
+
+/**
+ * @brief Returns current sample
+ *
+ * @param channel The ShmTDIQChannel structure.
+ *
+ * @return Current time as sample count since beginning of transmission
+ */
+uint64_t shm_td_iq_channel_get_current_sample(const ShmTDIQChannel *channel);
+
+#endif
diff --git a/common/utils/shm_iq_channel/tests/CMakeLists.txt b/common/utils/shm_iq_channel/tests/CMakeLists.txt
new file mode 100644
index 0000000000..f80f9cffbc
--- /dev/null
+++ b/common/utils/shm_iq_channel/tests/CMakeLists.txt
@@ -0,0 +1,5 @@
+add_executable(test_shm_td_iq_channel test_shm_td_iq_channel.c)
+target_link_libraries(test_shm_td_iq_channel shm_td_iq_channel minimal_lib pthread)
+add_dependencies(tests test_shm_td_iq_channel)
+# Commented out due to issues with ASAN in the unit test docker container
+#add_test(test_shm_td_iq_channel ./test_shm_td_iq_channel)
diff --git a/common/utils/shm_iq_channel/tests/test_shm_td_iq_channel.c b/common/utils/shm_iq_channel/tests/test_shm_td_iq_channel.c
new file mode 100644
index 0000000000..f5170b8144
--- /dev/null
+++ b/common/utils/shm_iq_channel/tests/test_shm_td_iq_channel.c
@@ -0,0 +1,169 @@
+/*
+ * Licensed to the OpenAirInterface (OAI) Software Alliance under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The OpenAirInterface Software Alliance licenses this file to You under
+ * the OAI Public License, Version 1.1 (the "License"); you may not use this file
+ * except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.openairinterface.org/?page_id=698
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *-------------------------------------------------------------------------------
+ * For more information about the OpenAirInterface (OAI) Software Alliance:
+ * contact@openairinterface.org
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include "assertions.h"
+
+#define SHM_CHANNEL_NAME "shm_iq_channel_test_file"
+#include "shm_td_iq_channel.h"
+
+void server(void);
+int client(void);
+
+enum { MODE_SERVER, MODE_CLIENT, MODE_FORK };
+int main(int argc, char *argv[])
+{
+ int mode = MODE_FORK;
+ if (argc == 2) {
+ if (strcmp(argv[1], "server") == 0) {
+ mode = MODE_SERVER;
+ } else if (strcmp(argv[1], "client") == 0) {
+ mode = MODE_CLIENT;
+ }
+ } else if (argc > 2) {
+ printf("Usage: %s [server|client]\n", argv[0]);
+ return 1;
+ }
+ switch (mode) {
+ case MODE_SERVER:
+ server();
+ break;
+ case MODE_CLIENT:
+ return client();
+ break;
+ case MODE_FORK: {
+ char file[256];
+ snprintf(file, sizeof(file), "/dev/shm/%s", SHM_CHANNEL_NAME);
+ remove(file);
+ int pid = fork();
+ if (pid == 0) {
+ server();
+ } else {
+ return client();
+ }
+ } break;
+ }
+
+ return 0;
+}
+
+const int num_updates = 50;
+const int num_samples_per_update = 30720;
+void *produce_symbols(void *arg)
+{
+ ShmTDIQChannel *channel = (ShmTDIQChannel *)arg;
+ for (int i = 0; i < num_updates; i++) {
+ shm_td_iq_channel_produce_samples(channel, num_samples_per_update);
+ usleep(20000);
+ }
+ return 0;
+}
+
+void server(void)
+{
+ int num_ant_tx = 1;
+ int num_ant_rx = 1;
+ ShmTDIQChannel *channel = shm_td_iq_channel_create(SHM_CHANNEL_NAME, num_ant_tx, num_ant_rx);
+ for (int i = 0; i < 10; i++) {
+ if (shm_td_iq_channel_is_connected(channel)) {
+ printf("Server connected\n");
+ break;
+ }
+ printf("Waiting for client\n");
+ sleep(1);
+ }
+ AssertFatal(shm_td_iq_channel_is_connected(channel), "Server failed to connect\n");
+
+ pthread_t producer_thread;
+ int ret = pthread_create(&producer_thread, NULL, produce_symbols, channel);
+ AssertFatal(ret == 0, "pthread_create() failed: errno %d, %s\n", errno, strerror(errno));
+
+ uint64_t timestamp = 0;
+ int iq_contents = 0;
+ while (timestamp < num_samples_per_update * num_updates) {
+ shm_td_iq_channel_wait(channel, timestamp + num_samples_per_update);
+ uint64_t target_timestamp = timestamp + num_samples_per_update;
+ timestamp += num_samples_per_update;
+ uint32_t iq_data[num_samples_per_update];
+ for (int i = 0; i < num_samples_per_update; i++) {
+ iq_data[i] = iq_contents;
+ }
+ int result = shm_td_iq_channel_tx(channel, target_timestamp, num_samples_per_update, 0, iq_data);
+ AssertFatal(result == CHANNEL_NO_ERROR, "Failed to write data\n");
+ iq_contents++;
+ }
+
+ printf("Finished writing data\n");
+
+ ret = pthread_join(producer_thread, NULL);
+ AssertFatal(ret == 0, "pthread_join() failed: errno %d, %s\n", errno, strerror(errno));
+ shm_td_iq_channel_destroy(channel);
+}
+
+int client(void)
+{
+ int total_errors = 0;
+ ShmTDIQChannel *channel = shm_td_iq_channel_connect(SHM_CHANNEL_NAME, 10);
+ for (int i = 0; i < 10; i++) {
+ if (shm_td_iq_channel_is_connected(channel)) {
+ printf("Client connected\n");
+ break;
+ }
+ printf("Waiting for server\n");
+ sleep(1);
+ }
+ AssertFatal(shm_td_iq_channel_is_connected(channel), "Client failed to connect\n");
+
+ uint64_t timestamp = 0;
+ int iq_contents = 0;
+
+ while (timestamp < num_samples_per_update * num_updates) {
+ shm_td_iq_channel_wait(channel, timestamp + num_samples_per_update);
+ // Server starts producing from second slot
+ if (timestamp > num_samples_per_update) {
+ uint64_t target_timestamp = timestamp - num_samples_per_update;
+ uint32_t iq_data[num_samples_per_update];
+ int result = shm_td_iq_channel_rx(channel, target_timestamp, num_samples_per_update, 0, iq_data);
+ AssertFatal(result == CHANNEL_NO_ERROR, "Failed to read data\n");
+ int num_errors = 0;
+ for (int i = 0; i < num_samples_per_update; i++) {
+ if (iq_data[i] != iq_contents) {
+ num_errors++;
+ }
+ }
+ if (num_errors) {
+ printf("Found %d errors, value = %d, reference = %d\n", num_errors, iq_data[0], iq_contents);
+ total_errors += num_errors;
+ }
+ iq_contents++;
+ }
+ timestamp += num_samples_per_update;
+ }
+
+ printf("Finished reading data\n");
+ shm_td_iq_channel_destroy(channel);
+ return total_errors;
+}
diff --git a/doc/README.md b/doc/README.md
index 4786458a66..cee2620f95 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -118,6 +118,7 @@ Some directories under `radio` contain READMEs:
- [BladeRF](../radio/BLADERF/README)
- [IQPlayer](../radio/iqplayer/DOC/iqrecordplayer_usage.md), and [general documentation](./iqrecordplayer_usage.md)
- [fhi_72](../radio/fhi_72/README.md)
+- [vrtsim](../radio/vrtsim/README.md)
The other SDRs (AW2S, LimeSDR, ...) have no READMEs.
diff --git a/docker/Dockerfile.gNB.rhel9 b/docker/Dockerfile.gNB.rhel9
index 894d1f6aa7..824e3b3833 100644
--- a/docker/Dockerfile.gNB.rhel9
+++ b/docker/Dockerfile.gNB.rhel9
@@ -72,6 +72,7 @@ COPY --from=gnb-build \
/oai-ran/cmake_targets/ran_build/build/libtelnetsrv.so \
/oai-ran/cmake_targets/ran_build/build/libtelnetsrv_ci.so \
/oai-ran/cmake_targets/ran_build/build/libparams_yaml.so \
+ /oai-ran/cmake_targets/ran_build/build/libvrtsim.so \
/usr/local/lib/
COPY --from=gnb-base \
diff --git a/docker/Dockerfile.gNB.rocky b/docker/Dockerfile.gNB.rocky
index 5c17c78e57..225432fdab 100644
--- a/docker/Dockerfile.gNB.rocky
+++ b/docker/Dockerfile.gNB.rocky
@@ -69,6 +69,7 @@ COPY --from=gnb-build \
/oai-ran/cmake_targets/ran_build/build/libtelnetsrv.so \
/oai-ran/cmake_targets/ran_build/build/libtelnetsrv_ci.so \
/oai-ran/cmake_targets/ran_build/build/libparams_yaml.so \
+ /oai-ran/cmake_targets/ran_build/build/libvrtsim.so \
/usr/local/lib/
COPY --from=gnb-base \
diff --git a/docker/Dockerfile.gNB.ubuntu22 b/docker/Dockerfile.gNB.ubuntu22
index da2074c64b..68c903450a 100644
--- a/docker/Dockerfile.gNB.ubuntu22
+++ b/docker/Dockerfile.gNB.ubuntu22
@@ -110,6 +110,7 @@ COPY --from=gnb-build \
/oai-ran/cmake_targets/ran_build/build/libtelnetsrv_5Gue.so \
/oai-ran/cmake_targets/ran_build/build/libtelnetsrv_rrc.so \
/oai-ran/cmake_targets/ran_build/build/libtelnetsrv_o1.so \
+ /oai-ran/cmake_targets/ran_build/build/libvrtsim.so \
/usr/local/lib/
# Now we are copying from builder-image the UHD files.
diff --git a/docker/Dockerfile.nrUE.rhel9 b/docker/Dockerfile.nrUE.rhel9
index 8ab486bd85..3a2900da27 100644
--- a/docker/Dockerfile.nrUE.rhel9
+++ b/docker/Dockerfile.nrUE.rhel9
@@ -75,6 +75,7 @@ COPY --from=nr-ue-build \
/oai-ran/cmake_targets/ran_build/build/libtelnetsrv.so \
/oai-ran/cmake_targets/ran_build/build/libtelnetsrv_5Gue.so \
/oai-ran/cmake_targets/ran_build/build/libparams_yaml.so \
+ /oai-ran/cmake_targets/ran_build/build/libvrtsim.so \
/usr/local/lib/
COPY --from=nr-ue-base \
diff --git a/docker/Dockerfile.nrUE.rocky b/docker/Dockerfile.nrUE.rocky
index d0455527fa..7f112af53a 100644
--- a/docker/Dockerfile.nrUE.rocky
+++ b/docker/Dockerfile.nrUE.rocky
@@ -72,6 +72,7 @@ COPY --from=nr-ue-build \
/oai-ran/cmake_targets/ran_build/build/libtelnetsrv.so \
/oai-ran/cmake_targets/ran_build/build/libtelnetsrv_5Gue.so \
/oai-ran/cmake_targets/ran_build/build/libparams_yaml.so \
+ /oai-ran/cmake_targets/ran_build/build/libvrtsim.so \
/usr/local/lib/
COPY --from=nr-ue-base \
diff --git a/docker/Dockerfile.nrUE.ubuntu22 b/docker/Dockerfile.nrUE.ubuntu22
index 45ad4bbe03..c3b6592753 100644
--- a/docker/Dockerfile.nrUE.ubuntu22
+++ b/docker/Dockerfile.nrUE.ubuntu22
@@ -110,6 +110,7 @@ COPY --from=nr-ue-build \
/oai-ran/cmake_targets/ran_build/build/libtelnetsrv_ciUE.so \
/oai-ran/cmake_targets/ran_build/build/libtelnetsrv_5Gue.so \
/oai-ran/cmake_targets/ran_build/build/libparams_yaml.so \
+ /oai-ran/cmake_targets/ran_build/build/libvrtsim.so \
/usr/local/lib/
# Now we are copying from builder-image the UHD files.
diff --git a/openair1/SIMULATION/TOOLS/sim.h b/openair1/SIMULATION/TOOLS/sim.h
index d6b20a9eb9..34cae4ba55 100644
--- a/openair1/SIMULATION/TOOLS/sim.h
+++ b/openair1/SIMULATION/TOOLS/sim.h
@@ -50,6 +50,7 @@ typedef enum {
#define CHANMODEL_FREE_RSQRT_6 1<<1
#define CHANMODEL_FREE_RSQRT_NTAPS 1<<2
#define CHANMODEL_FREE_AMPS 1<<3
+#define SHR3 (jz = jsr, jsr ^= (jsr << 13), jsr ^= (jsr >> 17), jsr ^= (jsr << 5), jz + jsr)
typedef enum {
CORR_LEVEL_LOW,
diff --git a/radio/CMakeLists.txt b/radio/CMakeLists.txt
index fb9086e2eb..654fb037f0 100644
--- a/radio/CMakeLists.txt
+++ b/radio/CMakeLists.txt
@@ -37,3 +37,8 @@ add_boolean_option(OAI_USRP OFF "Activate OAI's USRP driver" OFF)
if(OAI_USRP)
add_subdirectory(USRP)
endif()
+
+add_boolean_option(VRTSIM_RADIO ON "Activate OAI's shared memory radio driver" OFF)
+if(VRTSIM_RADIO)
+ add_subdirectory(vrtsim)
+endif()
diff --git a/radio/vrtsim/CMakeLists.txt b/radio/vrtsim/CMakeLists.txt
new file mode 100644
index 0000000000..79eb4d88da
--- /dev/null
+++ b/radio/vrtsim/CMakeLists.txt
@@ -0,0 +1,4 @@
+add_library(vrtsim MODULE vrtsim.c noise_device.c)
+set_target_properties(vrtsim PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
+target_link_libraries(vrtsim PRIVATE SIMU shm_td_iq_channel actor)
+add_dependencies(vrtsim generate_T)
diff --git a/radio/vrtsim/README.md b/radio/vrtsim/README.md
new file mode 100644
index 0000000000..71455175c3
--- /dev/null
+++ b/radio/vrtsim/README.md
@@ -0,0 +1,25 @@
+# Overview
+
+This implements a shared memory realtime and near-realtime radio interface.
+This is performed using `shm_td_iq_channel` which handles the shared memory
+interface.
+
+# Limitations
+
+ - Only 1gNB to 1nrUE: gNB as a server, UE as a client.
+ - Server only accepts connections during device bringup. This means that
+ restarting the client will not make it reconnect, the server has to be
+ restarted as well.
+
+# Usage
+
+On the UE and gNB: use `device.name vrtsim` command line argument.
+
+Additionally on gNB use `vrtsim.role server` and optionally
+`vrtsim.timescale ` to set the timescale. Timescale 1.0
+is the default and means realtime.
+
+Channel modelling can be enabled by adding `vrtsim.chanmod 1` to the
+command line and should work the same as channel modelling in rfsimulator,
+see rfsimulator [documentation](../rfsimulator/README.md), provided that your
+CPU is fast enough.
diff --git a/radio/vrtsim/noise_device.c b/radio/vrtsim/noise_device.c
new file mode 100644
index 0000000000..2e15944d4a
--- /dev/null
+++ b/radio/vrtsim/noise_device.c
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the OpenAirInterface (OAI) Software Alliance under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The OpenAirInterface Software Alliance licenses this file to You under
+ * the OAI Public License, Version 1.1 (the "License"); you may not use this file
+ * except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.openairinterface.org/?page_id=698
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *-------------------------------------------------------------------------------
+ * For more information about the OpenAirInterface (OAI) Software Alliance:
+ * contact@openairinterface.org
+ */
+#include
+#include
+#include "noise_device.h"
+
+// Use block size equal to cache size
+#define BLOCK_SIZE (64 / sizeof(float)) // 4 bytes per float
+// Use a power of 2 for block size length
+#define NUM_BLOCKS (8192)
+#define NOISE_VECTOR_LENGTH (BLOCK_SIZE * NUM_BLOCKS)
+
+static struct {
+ pthread_t noise_device_thread;
+ float noise[NOISE_VECTOR_LENGTH] __attribute__((aligned(64)));
+ float noise_power;
+ bool running;
+} state;
+
+static uint32_t jz, jsr = 123456789;
+static uint32_t random_uint(void)
+{
+ return SHR3;
+}
+
+static void generate_one_block_noise(int write_block_index, float noise_power)
+{
+ // Generate noise vector
+ float block[BLOCK_SIZE];
+ for (int i = 0; i < BLOCK_SIZE; i++) {
+ block[i] = noise_power * gaussZiggurat(0, 1);
+ }
+ // Write block to noise vector
+ memcpy(state.noise + write_block_index * BLOCK_SIZE, block, sizeof(block));
+}
+
+void *noise_device_thread_function(void *arg)
+{
+ while (state.running) {
+ // Generate noise vector
+ generate_one_block_noise(random_uint() % NUM_BLOCKS, state.noise_power);
+ usleep(10000);
+ }
+ return NULL;
+}
+
+void init_noise_device(float noise_power)
+{
+ for (int i = 0; i < NUM_BLOCKS; i++) {
+ generate_one_block_noise(i, noise_power);
+ }
+ int ret = pthread_create(&state.noise_device_thread, NULL, noise_device_thread_function, NULL);
+ AssertFatal(ret == 0, "pthread_create failed: %d, errno %d (%s)\n", ret, errno, strerror(errno));
+}
+
+void free_noise_device(void)
+{
+ state.running = false;
+ pthread_join(state.noise_device_thread, NULL);
+}
+
+void get_noise_vector(float *noise_vector, int length)
+{
+ int start_block = random_uint() % NUM_BLOCKS;
+ while (length > 0) {
+ int copy_size = min(length, (NUM_BLOCKS - start_block) * BLOCK_SIZE);
+ memcpy(noise_vector, &state.noise[start_block * BLOCK_SIZE], copy_size * sizeof(float));
+ start_block = 0;
+ length -= copy_size;
+ noise_vector += copy_size;
+ }
+}
diff --git a/radio/vrtsim/noise_device.h b/radio/vrtsim/noise_device.h
new file mode 100644
index 0000000000..421723891a
--- /dev/null
+++ b/radio/vrtsim/noise_device.h
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the OpenAirInterface (OAI) Software Alliance under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The OpenAirInterface Software Alliance licenses this file to You under
+ * the OAI Public License, Version 1.1 (the "License"); you may not use this file
+ * except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.openairinterface.org/?page_id=698
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *-------------------------------------------------------------------------------
+ * For more information about the OpenAirInterface (OAI) Software Alliance:
+ * contact@openairinterface.org
+ */
+
+#ifndef NOISE_DEVICE_H
+#define NOISE_DEVICE_H
+
+#include "SIMULATION/TOOLS/sim.h"
+
+void init_noise_device(float noise_power);
+void free_noise_device(void);
+void get_noise_vector(float *noise_vector, int length);
+
+#endif
diff --git a/radio/vrtsim/vrtsim.c b/radio/vrtsim/vrtsim.c
new file mode 100644
index 0000000000..f0e0151aa6
--- /dev/null
+++ b/radio/vrtsim/vrtsim.c
@@ -0,0 +1,598 @@
+/*
+ * Licensed to the OpenAirInterface (OAI) Software Alliance under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The OpenAirInterface Software Alliance licenses this file to You under
+ * the OAI Public License, Version 1.1 (the "License"); you may not use this file
+ * except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.openairinterface.org/?page_id=698
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *-------------------------------------------------------------------------------
+ * For more information about the OpenAirInterface (OAI) Software Alliance:
+ * contact@openairinterface.org
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include "common_lib.h"
+#include "shm_td_iq_channel.h"
+#include "SIMULATION/TOOLS/sim.h"
+#include "actor.h"
+#include "noise_device.h"
+#include "simde/x86/avx512.h"
+
+// Simulator role
+typedef enum { ROLE_SERVER = 1, ROLE_CLIENT } role;
+
+#define NUM_CHANMOD_THREADS 1
+
+#define ROLE_CLIENT_STRING "client"
+#define ROLE_SERVER_STRING "server"
+
+#define VRTSIM_SECTION "vrtsim"
+#define TIME_SCALE_HLP \
+ "sample time scale. 1.0 means realtime. Values > 1 mean faster than realtime. Values < 1 mean slower than realtime\n"
+
+// clang-format off
+#define VRTSIM_PARAMS_DESC \
+ { \
+ {"channel_name", "shared memory channel name\n", 0, .strptr = &channel_name, .defstrval = "vrtsim_channel", TYPE_STRING, 0}, \
+ {"role", "either client or server\n", 0, .strptr = &role, .defstrval = ROLE_CLIENT_STRING, TYPE_STRING, 0}, \
+ {"timescale", TIME_SCALE_HLP, 0, .dblptr = &vrtsim_state->timescale, .defdblval = 1.0, TYPE_DOUBLE, 0}, \
+ {"chanmod", "Enable channel modelling", 0, .iptr = &vrtsim_state->chanmod, .defintval = 0, TYPE_INT, 0}, \
+ };
+// clang-format on
+
+typedef struct histogram_s {
+ uint64_t diff[30];
+ int num_samples;
+ int min_samples;
+ double range;
+} histogram_t;
+
+// Information about the peer
+typedef struct peer_info_s {
+ int num_rx_antennas;
+} peer_info_t;
+
+typedef struct tx_timing_s {
+ uint64_t tx_samples_late;
+ uint64_t tx_early;
+ uint64_t tx_samples_total;
+ double average_tx_budget;
+ histogram_t tx_histogram;
+} tx_timing_t;
+
+typedef struct {
+ int role;
+ char channel_name[40];
+ ShmTDIQChannel *channel;
+ uint64_t last_received_sample;
+ pthread_t timing_thread;
+ bool run_timing_thread;
+ double timescale;
+ double sample_rate;
+ uint64_t rx_samples_late;
+ uint64_t rx_early;
+ uint64_t rx_samples_total;
+ tx_timing_t *tx_timing;
+ peer_info_t peer_info;
+ int chanmod;
+ double rx_freq;
+ double tx_bw;
+ int tx_num_channels;
+ int rx_num_channels;
+ channel_desc_t *channel_desc;
+ Actor_t *channel_modelling_actors;
+} vrtsim_state_t;
+
+static void histogram_add(histogram_t *histogram, double diff)
+{
+ histogram->num_samples++;
+ if (histogram->num_samples >= histogram->min_samples) {
+ int bin = min(sizeofArray(histogram->diff) - 1, max(0, (int)(diff / histogram->range * sizeofArray(histogram->diff))));
+ histogram->diff[bin]++;
+ }
+}
+
+static void histogram_print(histogram_t *histogram)
+{
+ LOG_I(HW, "VRTSIM: TX budget histogram: %d samples\n", histogram->num_samples);
+ float bin_size = histogram->range / sizeofArray(histogram->diff);
+ float bin_start = 0;
+ for (int i = 0; i < sizeofArray(histogram->diff); i++) {
+ LOG_I(HW, "Bin %d\t[%.1f - %.1fuS]:\t\t%lu\n", i, bin_start, bin_start + bin_size, histogram->diff[i]);
+ bin_start += bin_size;
+ }
+}
+
+static void histogram_merge(histogram_t *dest, histogram_t *src)
+{
+ for (int i = 0; i < sizeofArray(dest->diff); i++) {
+ dest->diff[i] += src->diff[i];
+ }
+ dest->num_samples += src->num_samples;
+}
+
+static void load_channel_model(vrtsim_state_t *vrtsim_state)
+{
+ load_channellist(vrtsim_state->tx_num_channels,
+ vrtsim_state->peer_info.num_rx_antennas,
+ vrtsim_state->sample_rate,
+ vrtsim_state->rx_freq,
+ vrtsim_state->tx_bw);
+ char *model_name = vrtsim_state->role == ROLE_CLIENT ? "client_tx_channel_model" : "server_tx_channel_model";
+ vrtsim_state->channel_desc = find_channel_desc_fromname(model_name);
+ random_channel(vrtsim_state->channel_desc, 0);
+ AssertFatal(vrtsim_state->channel_desc != NULL, "Could not find channel model %s\n", model_name);
+}
+
+static void vrtsim_readconfig(vrtsim_state_t *vrtsim_state)
+{
+ char *channel_name = NULL;
+ char *role = NULL;
+ paramdef_t vrtsim_params[] = VRTSIM_PARAMS_DESC;
+ int ret = config_get(config_get_if(), vrtsim_params, sizeofArray(vrtsim_params), VRTSIM_SECTION);
+ AssertFatal(ret >= 0, "configuration couldn't be performed\n");
+ strncpy(vrtsim_state->channel_name, channel_name, sizeof(vrtsim_state->channel_name) - 1);
+ if (strncmp(role, ROLE_CLIENT_STRING, strlen(ROLE_CLIENT_STRING)) == 0) {
+ vrtsim_state->role = ROLE_CLIENT;
+ } else if (strncmp(role, ROLE_SERVER_STRING, strlen(ROLE_SERVER_STRING)) == 0) {
+ vrtsim_state->role = ROLE_SERVER;
+ } else {
+ AssertFatal(false, "Invalid role configuration\n");
+ }
+}
+
+static void *vrtsim_timing_job(void *arg)
+{
+ vrtsim_state_t *vrtsim_state = arg;
+ struct timespec timestamp;
+ if (clock_gettime(CLOCK_REALTIME, ×tamp)) {
+ LOG_E(UTIL, "clock_gettime failed\n");
+ exit(1);
+ }
+ double leftover_samples = 0;
+ while (vrtsim_state->run_timing_thread) {
+ struct timespec current_time;
+ if (clock_gettime(CLOCK_REALTIME, ¤t_time)) {
+ LOG_E(UTIL, "clock_gettime failed\n");
+ exit(1);
+ }
+ uint64_t diff = (current_time.tv_sec - timestamp.tv_sec) * 1000000000 + (current_time.tv_nsec - timestamp.tv_nsec);
+ timestamp = current_time;
+ double samples_to_produce = vrtsim_state->sample_rate * vrtsim_state->timescale * diff / 1e9;
+
+ // Attempt to correct compounding rounding error
+ leftover_samples += samples_to_produce - (uint64_t)samples_to_produce;
+ if (leftover_samples > 1.0f) {
+ samples_to_produce += 1;
+ leftover_samples -= 1;
+ }
+ shm_td_iq_channel_produce_samples(vrtsim_state->channel, samples_to_produce);
+ usleep(1);
+ }
+ return 0;
+}
+
+/**
+ * @brief Exchanges peer information between the server and the client.
+ *
+ * This function sets up a Unix domain socket server, waits for a client to connect,
+ * and exchanges peer information (number of RX antennas) with the client.
+ *
+ * @param peer_info The peer information to send to the client.
+ * @return The peer information received from the client.
+ *
+ * @note This function will block until a client connects and the information is exchanged.
+ */
+static peer_info_t server_exchange_peer_info(peer_info_t peer_info)
+{
+ int server_fd, client_fd;
+
+ // Create a Unix domain socket
+ server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
+ AssertFatal(server_fd >= 0, "socket() failed: errno: %d, %s\n", errno, strerror(errno));
+
+ // Bind the socket to a file path
+ struct sockaddr_un addr = {.sun_family = AF_UNIX};
+ strncpy(addr.sun_path, "/tmp/vrtsim_socket", sizeof(addr.sun_path) - 1);
+ unlink(addr.sun_path); // Ensure the path does not already exist
+ int ret = bind(server_fd, (struct sockaddr *)&addr, sizeof(addr));
+ AssertFatal(ret == 0, "bind() failed: errno: %d, %s\n", errno, strerror(errno));
+
+ // Listen for incoming connections
+ ret = listen(server_fd, 1);
+ AssertFatal(ret == 0, "listen() failed: errno: %d, %s\n", errno, strerror(errno));
+
+ // Accept a connection from a client
+ socklen_t addr_len = sizeof(addr);
+ client_fd = accept(server_fd, (struct sockaddr *)&addr, &addr_len);
+ AssertFatal(client_fd >= 0, "accept() failed: errno: %d, %s\n", errno, strerror(errno));
+
+ // Send the antenna info to the server
+ ssize_t bytes_sent = send(client_fd, &peer_info, sizeof(peer_info), 0);
+ AssertFatal(bytes_sent == sizeof(peer_info), "send() failed: errno: %d, %s\n", errno, strerror(errno));
+
+ ssize_t bytes_received = recv(client_fd, &peer_info, sizeof(peer_info), 0);
+ AssertFatal(bytes_received == sizeof(peer_info), "recv() failed: errno: %d, %s\n", errno, strerror(errno));
+
+ LOG_I(HW, "Received client info: RX %d\n", peer_info.num_rx_antennas);
+
+ // Close the client and server sockets
+ close(client_fd);
+ close(server_fd);
+ return peer_info;
+}
+
+/**
+ * @brief Exchanges peer information between the client and the server.
+ *
+ * This function connects to a Unix domain socket server, exchanges peer information
+ * (number of RX antennas) with the server, and returns the peer information received
+ * from the server.
+ *
+ * @param peer_info The peer information to send to the server.
+ * @return The peer information received from the server.
+ *
+ * @note This function will block until the information is exchanged.
+ */
+static peer_info_t client_exchange_peer_info(peer_info_t peer_info)
+{
+ int client_fd;
+
+ // Create a Unix domain socket
+ client_fd = socket(AF_UNIX, SOCK_STREAM, 0);
+ AssertFatal(client_fd >= 0, "socket() failed: errno: %d, %s\n", errno, strerror(errno));
+
+ // Connect to the server
+ struct sockaddr_un addr = {.sun_family = AF_UNIX};
+ strncpy(addr.sun_path, "/tmp/vrtsim_socket", sizeof(addr.sun_path) - 1);
+ int ret = -1;
+ int tries = 0;
+ while (ret != 0 && tries < 10) {
+ ret = connect(client_fd, (struct sockaddr *)&addr, sizeof(addr));
+ tries++;
+ sleep(1);
+ }
+ AssertFatal(ret == 0, "connect() failed: errno: %d, %s\n", errno, strerror(errno));
+
+ peer_info_t peer_info_received;
+ ssize_t bytes_received = recv(client_fd, &peer_info_received, sizeof(peer_info_received), 0);
+ AssertFatal(bytes_received == sizeof(peer_info_received), "recv() failed: errno: %d, %s\n", errno, strerror(errno));
+
+ // Send the antenna info to the server
+ ssize_t bytes_sent = send(client_fd, &peer_info, sizeof(peer_info), 0);
+ AssertFatal(bytes_sent == sizeof(peer_info), "send() failed: errno: %d, %s\n", errno, strerror(errno));
+
+ LOG_I(HW, "Received peer info: RX %d\n", peer_info_received.num_rx_antennas);
+
+ // Close the client socket
+ close(client_fd);
+ return peer_info_received;
+}
+
+static int vrtsim_connect(openair0_device *device)
+{
+ vrtsim_state_t *vrtsim_state = (vrtsim_state_t *)device->priv;
+ // Exchange peer info
+ peer_info_t peer_info = {.num_rx_antennas = device->openair0_cfg[0].rx_num_channels};
+ if (vrtsim_state->role == ROLE_SERVER) {
+ vrtsim_state->peer_info = server_exchange_peer_info(peer_info);
+ } else {
+ vrtsim_state->peer_info = client_exchange_peer_info(peer_info);
+ }
+
+ // Handle channel modelling after number of RX antennas are known
+ int num_tx_stats = 1;
+ if (vrtsim_state->chanmod) {
+ 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++) {
+ init_actor(&vrtsim_state->channel_modelling_actors[i], "chanmod", -1);
+ }
+ load_channel_model(vrtsim_state);
+ num_tx_stats = vrtsim_state->peer_info.num_rx_antennas;
+ }
+ vrtsim_state->tx_timing = calloc_or_fail(num_tx_stats, sizeof(tx_timing_t));
+ for (int i = 0; i < num_tx_stats; i++) {
+ vrtsim_state->tx_timing[i].tx_histogram.min_samples = 100;
+ // Set the histogram range to 3000uS. Anything above that is not interesting
+ vrtsim_state->tx_timing[i].tx_histogram.range = 3000.0;
+ }
+
+ // Setup a shared memory channel
+ if (vrtsim_state->role == ROLE_SERVER) {
+ vrtsim_state->channel = shm_td_iq_channel_create(vrtsim_state->channel_name,
+ vrtsim_state->peer_info.num_rx_antennas,
+ device->openair0_cfg[0].rx_num_channels);
+ vrtsim_state->run_timing_thread = true;
+ while (!shm_td_iq_channel_is_connected(vrtsim_state->channel)) {
+ LOG_I(HW, "Waiting for client\n");
+ sleep(1);
+ }
+ int ret = pthread_create(&vrtsim_state->timing_thread, NULL, vrtsim_timing_job, vrtsim_state);
+ AssertFatal(ret == 0, "pthread_create() failed: errno: %d, %s\n", errno, strerror(errno));
+ } else {
+ vrtsim_state->channel = shm_td_iq_channel_connect(vrtsim_state->channel_name, 10);
+ }
+
+ return 0;
+}
+
+static int vrtsim_write_internal(vrtsim_state_t *vrtsim_state,
+ openair0_timestamp timestamp,
+ c16_t *samples,
+ int nsamps,
+ int aarx,
+ int flags,
+ int stats_index)
+{
+ tx_timing_t *tx_timing = &vrtsim_state->tx_timing[stats_index];
+
+ uint64_t sample = shm_td_iq_channel_get_current_sample(vrtsim_state->channel);
+ int64_t diff = timestamp - sample;
+ double budget = diff / (vrtsim_state->sample_rate / 1e6);
+ tx_timing->average_tx_budget = .05 * budget + .95 * tx_timing->average_tx_budget;
+ histogram_add(&tx_timing->tx_histogram, budget);
+
+ int ret = shm_td_iq_channel_tx(vrtsim_state->channel, timestamp, nsamps, aarx, (sample_t *)samples);
+
+ if (ret == CHANNEL_ERROR_TOO_LATE) {
+ tx_timing->tx_samples_late += nsamps;
+ } else if (ret == CHANNEL_ERROR_TOO_EARLY) {
+ tx_timing->tx_early += 1;
+ }
+ tx_timing->tx_samples_total += nsamps;
+
+ return nsamps;
+}
+
+typedef struct {
+ vrtsim_state_t *vrtsim_state;
+ openair0_timestamp timestamp;
+ c16_t *samples[4];
+ int nsamps;
+ int nbAnt;
+ int flags;
+ int aarx;
+} channel_modelling_args_t;
+
+static void perform_channel_modelling(void *arg)
+{
+ channel_modelling_args_t *channel_modelling_args = arg;
+ int nsamps = channel_modelling_args->nsamps;
+ int aarx = channel_modelling_args->aarx;
+ int nb_tx_ant = channel_modelling_args->nbAnt;
+ c16_t **input_samples = (c16_t **)channel_modelling_args->samples;
+
+ int aligned_nsamps = ceil_mod(nsamps, (512 / 8) / sizeof(cf_t));
+ cf_t samples[aligned_nsamps] __attribute__((aligned(64)));
+ // Apply noise from global settings
+ get_noise_vector((float *)samples, nsamps * 2);
+
+ channel_desc_t *channel_desc = channel_modelling_args->vrtsim_state->channel_desc;
+ const float pathloss_linear = powf(10, channel_desc->path_loss_dB / 20.0);
+
+ // Convert channel impulse response to float + apply pathloss
+ cf_t channel_impulse_response[nb_tx_ant][channel_desc->channel_length];
+ for (int aatx = 0; aatx < nb_tx_ant; aatx++) {
+ const struct complexd *channelModel = channel_desc->ch[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;
+ }
+ }
+
+ for (int aatx = 0; aatx < nb_tx_ant; aatx++) {
+ for (int i = 0; i < nsamps; i++) {
+ cf_t *impulse_response = channel_impulse_response[aatx];
+ for (int l = 0; l < channel_desc->channel_length; l++) {
+ int idx = i - l;
+ // TODO: Use AVX512 for this
+ // TODO: What are the previously sent samples (for impulse response)
+ if (idx >= 0) {
+ c16_t tx_input = input_samples[aatx][idx];
+ samples[i].r += tx_input.r * impulse_response[l].r - tx_input.i * impulse_response[l].i;
+ samples[i].i += tx_input.i * impulse_response[l].r + tx_input.r * impulse_response[l].i;
+ }
+ }
+ }
+ }
+
+ // Convert to c16_t
+ c16_t samples_out[aligned_nsamps] __attribute__((aligned(64)));
+#if defined(__AVX512F__)
+ for (int i = 0; i < aligned_nsamps / 8; i++) {
+ simde__m512 *in = (simde__m512 *)&samples[i * 8];
+ simde__m256i *out = (simde__m256i *)&samples_out[i * 8];
+ *out = simde_mm512_cvtsepi32_epi16(simde_mm512_cvtps_epi32(*in));
+ }
+#elif defined(__AVX2__)
+ for (int i = 0; i < aligned_nsamps / 4; i++) {
+ simde__m256 *in = (simde__m256 *)&samples[i * 4];
+ simde__m128i *out = (simde__m128i *)&samples_out[i * 4];
+ *out = simde_mm256_cvtsepi32_epi16(simde_mm256_cvtps_epi32(*in));
+ }
+#else
+ for (int i = 0; i < nsamps; i++) {
+ samples_out[i].r = lroundf(samples[i].r);
+ samples_out[i].i = lroundf(samples[i].i);
+ }
+#endif
+
+ vrtsim_write_internal(channel_modelling_args->vrtsim_state,
+ channel_modelling_args->timestamp,
+ samples_out,
+ channel_modelling_args->nsamps,
+ aarx,
+ channel_modelling_args->flags,
+ aarx);
+}
+
+static int vrtsim_write_with_chanmod(vrtsim_state_t *vrtsim_state,
+ openair0_timestamp timestamp,
+ void **samplesVoid,
+ int nsamps,
+ int nbAnt,
+ int flags)
+{
+ for (int aarx = 0; aarx < vrtsim_state->peer_info.num_rx_antennas; aarx++) {
+ notifiedFIFO_elt_t *task = newNotifiedFIFO_elt(sizeof(channel_modelling_args_t), 0, NULL, perform_channel_modelling);
+ channel_modelling_args_t *args = (channel_modelling_args_t *)NotifiedFifoData(task);
+ args->vrtsim_state = vrtsim_state;
+ args->timestamp = timestamp;
+ args->nsamps = nsamps;
+ args->nbAnt = nbAnt;
+ args->flags = flags;
+ args->aarx = aarx;
+ for (int i = 0; i < nbAnt; i++) {
+ args->samples[i] = samplesVoid[i];
+ }
+ pushNotifiedFIFO(&vrtsim_state->channel_modelling_actors[aarx].fifo, task);
+ }
+ return nsamps;
+}
+
+static int vrtsim_write(openair0_device *device, openair0_timestamp timestamp, void **samplesVoid, int nsamps, int nbAnt, int flags)
+{
+ timestamp -= device->openair0_cfg->command_line_sample_advance;
+ vrtsim_state_t *vrtsim_state = (vrtsim_state_t *)device->priv;
+ return vrtsim_state->chanmod ? 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);
+}
+
+static int vrtsim_read(openair0_device *device, openair0_timestamp *ptimestamp, void **samplesVoid, int nsamps, int nbAnt)
+{
+ vrtsim_state_t *vrtsim_state = (vrtsim_state_t *)device->priv;
+ shm_td_iq_channel_wait(vrtsim_state->channel, vrtsim_state->last_received_sample + nsamps);
+ 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;
+ }
+ vrtsim_state->rx_samples_total += nsamps;
+ *ptimestamp = vrtsim_state->last_received_sample;
+ vrtsim_state->last_received_sample += nsamps;
+ return nsamps;
+}
+
+static void vrtsim_end(openair0_device *device)
+{
+ vrtsim_state_t *vrtsim_state = (vrtsim_state_t *)device->priv;
+ if (vrtsim_state->role == ROLE_SERVER) {
+ vrtsim_state->run_timing_thread = false;
+ int ret = pthread_join(vrtsim_state->timing_thread, NULL);
+ AssertFatal(ret == 0, "pthread_join() failed: errno: %d, %s\n", errno, strerror(errno));
+ }
+
+ tx_timing_t *tx_timing = vrtsim_state->tx_timing;
+ if (vrtsim_state->chanmod) {
+ for (int i = 0; i < vrtsim_state->peer_info.num_rx_antennas; i++) {
+ shutdown_actor(&vrtsim_state->channel_modelling_actors[i]);
+ }
+ free(vrtsim_state->channel_modelling_actors);
+ 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;
+ tx_timing->tx_samples_late += tx_timing[i].tx_samples_late;
+ tx_timing->average_tx_budget += tx_timing[i].average_tx_budget;
+ tx_timing->tx_samples_total += tx_timing[i].tx_samples_total;
+ }
+ tx_timing->average_tx_budget /= vrtsim_state->peer_info.num_rx_antennas;
+ }
+ // produce 1 second of extra samples so threads can finish
+ shm_td_iq_channel_produce_samples(vrtsim_state->channel, vrtsim_state->sample_rate);
+ sleep(1);
+ shm_td_iq_channel_destroy(vrtsim_state->channel);
+
+ LOG_I(HW,
+ "VRTSIM: Realtime issues: TX %.2f%%, RX %.2f%%\n",
+ tx_timing->tx_samples_late / (float)tx_timing->tx_samples_total * 100,
+ vrtsim_state->rx_samples_late / (float)vrtsim_state->rx_samples_total * 100);
+ LOG_I(HW,
+ "VRTSIM: Read/write too early (suspected radio implementaton error) TX: %lu, RX: %lu\n",
+ tx_timing->tx_early,
+ vrtsim_state->rx_early);
+ LOG_I(HW, "VRTSIM: Average TX budget %.3lf uS\n", tx_timing->average_tx_budget);
+ histogram_print(&tx_timing->tx_histogram);
+ free(vrtsim_state->tx_timing);
+}
+
+static int vrtsim_stub(openair0_device *device)
+{
+ return 0;
+}
+static int vrtsim_stub2(openair0_device *device, openair0_config_t *openair0_cfg)
+{
+ return 0;
+}
+
+static int vrtsim_set_freq(openair0_device *device, openair0_config_t *openair0_cfg)
+{
+ vrtsim_state_t *s = device->priv;
+ s->rx_freq = openair0_cfg->rx_freq[0];
+ return 0;
+}
+
+__attribute__((__visibility__("default"))) int device_init(openair0_device *device, openair0_config_t *openair0_cfg)
+{
+ vrtsim_state_t *vrtsim_state = calloc_or_fail(1, sizeof(vrtsim_state_t));
+ vrtsim_readconfig(vrtsim_state);
+ LOG_I(HW,
+ "Running as %s\n",
+ vrtsim_state->role == ROLE_SERVER ? "server: waiting for client to connect" : "client: will connect to a vrtsim server");
+ device->trx_start_func = vrtsim_connect;
+ device->trx_reset_stats_func = vrtsim_stub;
+ device->trx_end_func = vrtsim_end;
+ device->trx_stop_func = vrtsim_stub;
+ device->trx_set_freq_func = vrtsim_set_freq;
+ device->trx_set_gains_func = vrtsim_stub2;
+ device->trx_write_func = vrtsim_write;
+ device->trx_read_func = vrtsim_read;
+
+ device->type = RFSIMULATOR;
+ device->openair0_cfg = &openair0_cfg[0];
+ device->priv = vrtsim_state;
+ device->trx_write_init = vrtsim_stub;
+ vrtsim_state->last_received_sample = 0;
+ vrtsim_state->sample_rate = openair0_cfg->sample_rate;
+ vrtsim_state->rx_freq = openair0_cfg->rx_freq[0];
+ vrtsim_state->tx_bw = openair0_cfg->tx_bw;
+ vrtsim_state->tx_num_channels = openair0_cfg->tx_num_channels;
+ vrtsim_state->rx_num_channels = openair0_cfg->rx_num_channels;
+
+ if (vrtsim_state->chanmod) {
+ init_channelmod();
+ int noise_power_dBFS = get_noise_power_dBFS();
+ int16_t noise_power = noise_power_dBFS == INVALID_DBFS_VALUE ? 0 : (int16_t)(32767.0 / powf(10.0, .05 * -noise_power_dBFS));
+ init_noise_device(noise_power);
+ }
+ return 0;
+}