ZMQ radio

This commit introduces ZMQ-based radio library. Each pair of RX/TX antennas is
represented by a ZMQ REQ/REP socket pair which streams continuous IQ samples
from radio start until stop.

Usage:
Simplest configuration is to connect OAI NR UE to OAI GNB with the same number
of antennas - by inverting the RX and TX channels in ZMQ radio configuration the
gNBs TX is mapped to UEs RX antennas and vice versa.
This commit is contained in:
Bartosz Podrygajlo
2026-03-24 14:01:14 +01:00
parent 38dc378224
commit 46c120651f
17 changed files with 1246 additions and 6 deletions

8
NOTICE
View File

@@ -84,3 +84,11 @@ Nuand: https://github.com/Nuand/bladeRF/tree/master?tab=License-1-ov-file
Credits for https://github.com/pothosware/SoapySDR
Pothosware: BSL 1.0 License
Credits for https://github.com/zeromq/libzmq
ZeroMQ authors: Mozilla Public License Version 2.0
Credits for source code:
- radio/zmq/zmq_imported.cpp
- radio/zmq/zmq_imported.h
Software Radio Systems Limited: BSD-3-Clause-Open-MPI

View File

@@ -13,11 +13,12 @@ RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get upgrade --yes && \
DEBIAN_FRONTEND=noninteractive apt-get install --yes \
libgtest-dev \
libyaml-cpp-dev
libyaml-cpp-dev \
libzmq3-dev
RUN rm -Rf /oai-ran
WORKDIR /oai-ran
COPY . .
WORKDIR /oai-ran/build
RUN cmake -GNinja -DENABLE_TESTS=ON -DCMAKE_BUILD_TYPE=Debug -DSANITIZE_ADDRESS=True .. && ninja tests
RUN cmake -GNinja -DENABLE_TESTS=ON -DOAI_ZMQ=ON -DCMAKE_BUILD_TYPE=Debug -DSANITIZE_ADDRESS=True .. && ninja tests

View File

@@ -282,12 +282,9 @@ configmodule_interface_t *load_configmodule(int argc,
cfgmode = strdup(CONFIG_LIBCONFIGFILE);
}
}
static configmodule_interface_t *cfgptr = NULL;
if (cfgptr)
fprintf(stderr, "ERROR: Call load_configmodule more than one time\n");
// The macros are not thread safe print_params and similar
cfgptr = calloc(sizeof(configmodule_interface_t), 1);
configmodule_interface_t *cfgptr = calloc(sizeof(configmodule_interface_t), 1);
if (!cfgptr) {
fprintf(stderr, "ERROR: cannot allocate a memory for configuration\n");
return NULL;

View File

@@ -10,6 +10,9 @@
#ifndef INCLUDE_CONFIG_LOADCONFIGMODULE_H
#define INCLUDE_CONFIG_LOADCONFIGMODULE_H
#ifdef __cplusplus
extern "C" {
#endif
#include <string.h>
#include <stdlib.h>
@@ -117,5 +120,8 @@ void write_parsedcfg(configmodule_interface_t *cfg);
extern void free_configmodule(void);
#define CONFIG_PRINTF_ERROR(f, x... ) if (isLogInitDone ()) { LOG_E(ENB_APP,f,x);} else {printf(f,x);}; if ( !CONFIG_ISFLAGSET(CONFIG_NOABORTONCHKF) ) exit_fun("exit because configuration failed\n");
#ifdef __cplusplus
}
#endif
#endif /* INCLUDE_CONFIG_LOADCONFIGMODULE_H */

View File

@@ -181,6 +181,11 @@ typedef struct paramdef {
{ \
OPTNAME(name), HELPSTR(help), PARAMFLAG(flags), .u64ptr = ptr, .defuintval = defval, PARAMTYPE(TYPE_UINT64), .numelt = 0 \
}
#define STRINGLISTPARAM(name, help, flags, ptr, defval) \
{ \
OPTNAME(name), HELPSTR(help), PARAMFLAG(flags), .strptr = ptr, .defstrlistval = defval, PARAMTYPE(TYPE_STRINGLIST), \
.numelt = 0 \
}
typedef struct paramlist_def {
char listname[MAX_OPTNAME_SIZE];

View File

@@ -108,6 +108,7 @@ Some directories under `radio` contain READMEs:
- [fhi_72](../radio/fhi_72/README.md)
- [vrtsim](../radio/vrtsim/README.md)
- [rf_emulator](../radio/emulator/README.md)
- [zmq](../radio/zmq/README.md)
The other SDRs (AW2S, LimeSDR, ...) have no READMEs.

View File

@@ -51,3 +51,8 @@ add_boolean_option(OAI_RF_EMULATOR ON "Activate OAI's RF emulator" OFF)
if(OAI_RF_EMULATOR)
add_subdirectory(emulator)
endif()
add_boolean_option(OAI_ZMQ OFF "Activate OAI's ZMQ radio" OFF)
if(OAI_ZMQ)
add_subdirectory(zmq)
endif()

21
radio/zmq/CMakeLists.txt Normal file
View File

@@ -0,0 +1,21 @@
# SPDX-License-Identifier: LicenseRef-CSSL-1.0
pkg_check_modules(zmq REQUIRED libzmq)
add_library(ring_buffer ring_buffer.cpp)
add_library(zmq_imported zmq_imported.cpp)
target_include_directories(zmq_imported PUBLIC ./)
target_link_libraries(zmq_imported PUBLIC ${zmq_LIBRARIES} log_headers)
target_include_directories(ring_buffer PUBLIC ./)
add_library(oai_zmqdevif SHARED zmq_radio.cpp)
target_link_libraries(oai_zmqdevif PUBLIC ${zmq_LIBRARIES} ring_buffer log_headers zmq_imported)
target_include_directories(oai_zmqdevif PUBLIC ${zmq_INCLUDE_DIRS})
set_target_properties(oai_zmqdevif PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
add_library(zmq_radio_static STATIC zmq_radio.cpp)
target_link_libraries(zmq_radio_static PUBLIC ${zmq_LIBRARIES} ring_buffer log_headers zmq_imported)
target_include_directories(zmq_radio_static PUBLIC ${zmq_INCLUDE_DIRS})
if (ENABLE_TESTS)
add_subdirectory(tests)
endif()

40
radio/zmq/README.md Normal file
View File

@@ -0,0 +1,40 @@
<!-- SPDX-License-Identifier: CC-BY-4.0 -->
# Overview
This library implements ZMQ-based radio driver
# Architecture
The radio simulates RX/TX pairs between different processes over network or on the same machine
as ZMQ REQ/REP socket pairs. All the antennas have to be configured and connected before the
simulation can start.
# Limitations
You cannot reconnect a device once the simulation has started. The simulation has to be restarted
from scratch in order to add devices or restart a process.
# Requirements
Depends on zmq library being installed in the system. On ubuntu: `libzmq3-dev`, on RHEL/Fedora: `zeromq-devel`
# Usage
## Simple 1-to-1 antenna mapping
Add `--device.name oai_zmqdevif` to load the library in UE / gNB process. Add `--zmq.[0].tx_channels <channels>` and
`--zmq.[0].rx_channels <channels>` to define ZMQ REQ/REP pairs.
On the opposite side, load the library like specified above but invert the `rx` and `tx` channels. This way all
antennas of UE will be directly mapped to all antennas of the gNB.
### Example:
```
sudo ./nr-uesoftmodem -r 106 --numerology 1 --band 78 -C 3619200000 --device.name oai_zmqdevif --zmq.[0].tx_channels tcp://127.0.0.1:4557 --zmq.[0].rx_channels tcp://127.0.0.1:4556 --ssb 516
```
```
sudo ./nr-softmodem -O ../targets/PROJECTS/GENERIC-NR-5GC/CONF/gnb.sa.band78.fr1.106PRB.usrpb210.conf --gNBs.[0].min_rxtxtime 6 --device.name oai_zmqdevif --zmq.[0].tx_channels tcp://127.0.0.1:4556 --zmq.[0].rx_channels tcp://127.0.0.1:4557
```

167
radio/zmq/ring_buffer.cpp Normal file
View File

@@ -0,0 +1,167 @@
/*
* SPDX-License-Identifier: LicenseRef-CSSL-1.0
*/
#include "ring_buffer.h"
#include <cstring>
#include <iostream>
#include <algorithm>
ring_buffer::ring_buffer(size_t max_size) : max_size_(max_size)
{
buffer_ = std::make_unique<cf_t[]>(max_size);
}
size_t ring_buffer::push_samples(const cf_t *samples, const size_t nsamps)
{
size_t overflow = 0;
// if nsamps > max_size skip nsamps - max_size samples
size_t nsamps_left = nsamps;
if (nsamps > max_size_) {
samples += nsamps - max_size_;
nsamps_left = max_size_;
overflow += nsamps - max_size_;
}
// Detect overflow
if (size_ + nsamps_left > max_size_) {
size_t newtail__pos = (head_ + nsamps_left) % max_size_;
overflow += (size_ + nsamps_left) - max_size_;
tail_ = newtail__pos;
}
size_t first_chunk = std::min(nsamps_left, max_size_ - head_);
memcpy(&buffer_[head_], samples, first_chunk * sizeof(cf_t));
head_ = (head_ + first_chunk) % max_size_;
samples += first_chunk;
nsamps_left -= first_chunk;
if (nsamps_left > 0) {
memcpy(&buffer_[0], samples, nsamps_left * sizeof(cf_t));
head_ = nsamps_left;
}
size_ = std::min(size_ + nsamps, max_size_);
return overflow;
}
size_t ring_buffer::push_zeros(const size_t num_zeros)
{
size_t overflow = 0;
// if nsamps > max_size skip nsamps - max_size samples
size_t nsamps_left = num_zeros;
if (num_zeros > max_size_) {
nsamps_left = max_size_;
overflow += num_zeros - max_size_;
}
// Detect overflow
if (size_ + nsamps_left > max_size_) {
size_t new_tail_pos = (head_ + nsamps_left) % max_size_;
overflow += (size_ + nsamps_left) - max_size_;
tail_ = new_tail_pos;
}
size_t first_chunk = std::min(nsamps_left, max_size_ - head_);
memset(&buffer_[head_], 0, first_chunk * sizeof(cf_t));
head_ = (head_ + first_chunk) % max_size_;
nsamps_left -= first_chunk;
if (nsamps_left > 0) {
memset(&buffer_[0], 0, nsamps_left * sizeof(cf_t));
head_ = nsamps_left;
}
size_ = std::min(size_ + num_zeros, max_size_);
return overflow;
}
size_t ring_buffer::pop_samples(cf_t *samples, size_t num_samples)
{
size_t samples_to_pop = std::min(size_, num_samples);
if (samples_to_pop > 0) {
if (tail_ + samples_to_pop > max_size_) {
size_t first_chunk = max_size_ - tail_;
memcpy(samples, &buffer_[tail_], first_chunk * sizeof(cf_t));
memcpy(samples + first_chunk, &buffer_[0], (samples_to_pop - first_chunk) * sizeof(cf_t));
} else {
memcpy(samples, &buffer_[tail_], samples_to_pop * sizeof(cf_t));
}
tail_ = (tail_ + samples_to_pop) % max_size_;
size_ -= samples_to_pop;
return samples_to_pop;
}
return 0;
}
void ring_buffer::clear_samples()
{
head_ = 0;
tail_ = 0;
size_ = 0;
}
void ring_buffer::reset()
{
clear_samples();
}
size_t ring_buffer::size() const
{
return size_;
}
size_t overflow_buffer::push_samples(const cf_t *samples, size_t nsamps)
{
std::lock_guard<std::mutex> lock(mutex_);
size_t overflow = buffer_.push_samples(samples, nsamps);
zeros_to_send_ += overflow;
return overflow;
}
size_t overflow_buffer::push_zeros(size_t num_zeros)
{
std::lock_guard<std::mutex> lock(mutex_);
size_t overflow = buffer_.push_zeros(num_zeros);
zeros_to_send_ += overflow;
return overflow;
}
size_t overflow_buffer::pop_samples(cf_t *samples, size_t num_samples)
{
std::lock_guard<std::mutex> lock(mutex_);
size_t samples_popped = 0;
if (zeros_to_send_ > 0) {
size_t num_zeros = std::min(zeros_to_send_, num_samples);
memset(samples, 0, num_zeros * sizeof(cf_t));
zeros_to_send_ -= num_zeros;
samples += num_zeros;
num_samples -= num_zeros;
samples_popped += num_zeros;
}
if (num_samples > 0) {
samples_popped += buffer_.pop_samples(samples, num_samples);
}
return samples_popped;
}
void overflow_buffer::reset()
{
std::lock_guard<std::mutex> lock(mutex_);
buffer_.reset();
zeros_to_send_ = buffer_.size() / 2;
}
void overflow_buffer::clear_samples()
{
std::lock_guard<std::mutex> lock(mutex_);
buffer_.clear_samples();
zeros_to_send_ = 0;
}
size_t overflow_buffer::size()
{
std::lock_guard<std::mutex> lock(mutex_);
return buffer_.size() + zeros_to_send_;
}

46
radio/zmq/ring_buffer.h Normal file
View File

@@ -0,0 +1,46 @@
/*
* SPDX-License-Identifier: LicenseRef-CSSL-1.0
*/
#pragma once
#include <vector>
#include <mutex>
#include <memory>
#include "common/platform_types.h"
// A basic cirular sample buffer class
class ring_buffer {
private:
std::unique_ptr<cf_t[]> buffer_;
size_t head_ = 0;
size_t tail_ = 0;
size_t size_ = 0;
size_t max_size_;
public:
ring_buffer(size_t max_size = 614400);
size_t push_samples(const cf_t *samples, size_t nsamps);
size_t push_zeros(size_t num_zeros);
size_t pop_samples(cf_t *samples, size_t num_samples);
void reset();
void clear_samples();
size_t size() const;
};
// A thread-safe wrapper around ring_buffer that counts overflows
class overflow_buffer {
ring_buffer buffer_;
size_t zeros_to_send_;
std::mutex mutex_;
public:
overflow_buffer(size_t max_size = 614400) : buffer_(max_size), zeros_to_send_(0)
{
}
size_t push_samples(const cf_t *samples, size_t nsamps);
size_t push_zeros(size_t num_zeros);
size_t pop_samples(cf_t *samples, size_t num_samples);
void reset();
void clear_samples();
size_t size();
};

View File

@@ -0,0 +1,13 @@
# SPDX-License-Identifier: LicenseRef-CSSL-1.0
add_executable(test_ring_buffer test_ring_buffer.cpp)
target_link_libraries(test_ring_buffer PRIVATE ring_buffer GTest::gtest)
add_dependencies(tests test_ring_buffer)
add_test(NAME test_ring_buffer
COMMAND ./test_ring_buffer)
add_executable(test_zmq_radio test_zmq_radio.cpp)
target_link_libraries(test_zmq_radio PRIVATE zmq_radio_static UTIL CONFIG_LIB GTest::gtest zmq)
add_dependencies(tests test_zmq_radio)
add_test(NAME test_zmq_radio
COMMAND ./test_zmq_radio)

View File

@@ -0,0 +1,221 @@
/*
* SPDX-License-Identifier: LicenseRef-CSSL-1.0
*/
#include <gtest/gtest.h>
#include "ring_buffer.h"
TEST(CircularBuffer, simplePushPop)
{
ring_buffer cb(10);
cf_t data[10];
for (int i = 0; i < 10; i++) {
data[i] = { (float)i, (float)(i + 1)};
}
ASSERT_EQ(cb.push_samples(data, 10), 0);
cf_t read_data[10];
ASSERT_EQ(cb.pop_samples(read_data, 10), 10);
for (int i = 0; i < 10; i++) {
ASSERT_EQ(read_data[i].r, data[i].r);
ASSERT_EQ(read_data[i].i, data[i].i);
}
ASSERT_EQ(cb.size(), 0);
ASSERT_EQ(cb.pop_samples(read_data, 10), 0);
}
TEST(CircularBuffer, overflow)
{
ring_buffer cb(10);
cf_t data[15];
for (int i = 0; i < 15; i++) {
data[i] = {(float)i, (float)(i + 1)};
}
ASSERT_EQ(cb.push_samples(data, 15), 5);
cf_t read_data[10];
ASSERT_EQ(cb.pop_samples(read_data, 10), 10);
// The first 5 samples are lost due to overflow
for (int i = 0; i < 10; i++) {
ASSERT_EQ(read_data[i].r, i + 5);
ASSERT_EQ(read_data[i].i, i + 6);
}
}
TEST(CircularBuffer, partialPop)
{
ring_buffer cb(10);
cf_t data[10];
for (int i = 0; i < 10; i++) {
data[i] = {(float)i, (float)(i + 1)};
}
ASSERT_EQ(cb.push_samples(data, 10), 0);
cf_t read_data[5];
ASSERT_EQ(cb.pop_samples(read_data, 5), 5);
for (int i = 0; i < 5; i++) {
ASSERT_EQ(read_data[i].r, i);
ASSERT_EQ(read_data[i].i, i + 1);
}
ASSERT_EQ(cb.size(), 5);
ASSERT_EQ(cb.pop_samples(read_data, 5), 5);
for (int i = 0; i < 5; i++) {
ASSERT_EQ(read_data[i].r, i + 5);
ASSERT_EQ(read_data[i].i, i + 6);
}
}
TEST(CircularBuffer, popMore)
{
ring_buffer cb(10);
cf_t data[10];
for (int i = 0; i < 10; i++) {
data[i] = {(float)i, (float)(i + 1)};
}
cb.push_samples(data, 10);
cf_t read_data[15];
ASSERT_EQ(cb.pop_samples(read_data, 15), 10);
for (int i = 0; i < 10; i++) {
ASSERT_EQ(read_data[i].r, i);
ASSERT_EQ(read_data[i].i, i + 1);
}
}
TEST(CircularBuffer, newOverflow)
{
ring_buffer cb(10);
cf_t data1[8];
for (int i = 0; i < 8; i++) {
data1[i] = {(float)i, (float)(i + 1)};
}
ASSERT_EQ(cb.push_samples(data1, 8), 0);
cf_t data2[4];
for (int i = 0; i < 4; i++) {
data2[i] = {(float)(i + 8), (float)(i + 9)};
}
ASSERT_EQ(cb.push_samples(data2, 4), 2);
cf_t read_data[10];
ASSERT_EQ(cb.pop_samples(read_data, 10), 10);
// The first 2 samples are lost due to overflow
for (int i = 0; i < 8; i++) {
ASSERT_EQ(read_data[i].r, i + 2);
ASSERT_EQ(read_data[i].i, i + 3);
}
ASSERT_EQ(read_data[8].r, 10);
ASSERT_EQ(read_data[8].i, 11);
ASSERT_EQ(read_data[9].r, 11);
ASSERT_EQ(read_data[9].i, 12);
}
TEST(overflow_buffer, simplePushPop)
{
overflow_buffer pb(10);
cf_t data[10];
for (int i = 0; i < 10; i++) {
data[i] = {(float)i, (float)(i + 1)};
}
pb.push_samples(data, 10);
ASSERT_EQ(pb.size(), 10);
cf_t read_data[10];
ASSERT_EQ(pb.pop_samples(read_data, 10), 10);
for (int i = 0; i < 10; i++) {
ASSERT_EQ(read_data[i].r, i);
ASSERT_EQ(read_data[i].i, i + 1);
}
}
TEST(overflow_buffer, zeros_to_send)
{
overflow_buffer pb(10);
pb.push_zeros(10);
cf_t data[5];
for (int i = 0; i < 5; i++) {
data[i] = {(float)i, (float)(i + 1)};
}
pb.push_samples(data, 5);
ASSERT_EQ(pb.size(), 15);
cf_t read_data[10];
ASSERT_EQ(pb.pop_samples(read_data, 10), 10);
for (int i = 0; i < 10; i++) {
ASSERT_EQ(read_data[i].r, 0);
ASSERT_EQ(read_data[i].i, 0);
}
ASSERT_EQ(pb.pop_samples(read_data, 10), 5);
for (int i = 0; i < 5; i++) {
ASSERT_EQ(read_data[i].r, i);
ASSERT_EQ(read_data[i].i, i + 1);
}
}
TEST(overflow_buffer, no_zeros_to_send)
{
overflow_buffer pb(10);
cf_t data[10];
for (int i = 0; i < 10; i++) {
data[i] = {(float)i, (float)(i + 1)};
}
pb.push_samples(data, 10);
ASSERT_EQ(pb.size(), 10);
cf_t read_data[10];
ASSERT_EQ(pb.pop_samples(read_data, 10), 10);
for (int i = 0; i < 10; i++) {
ASSERT_EQ(read_data[i].r, i);
ASSERT_EQ(read_data[i].i, i + 1);
}
}
TEST(overflow_buffer, overflow)
{
overflow_buffer pb(10);
cf_t data[15];
for (int i = 0; i < 15; i++) {
data[i] = {(float)i, (float)(i + 1)};
}
pb.push_samples(data, 15);
ASSERT_EQ(pb.size(), 15);
cf_t read_data[15];
ASSERT_EQ(pb.pop_samples(read_data, 15), 15);
// The first 5 samples were lost due to overflow.
int i = 0;
for (; i < 5; i++) {
ASSERT_EQ(read_data[i].r, 0);
ASSERT_EQ(read_data[i].i, 0);
}
for (; i < 15; i++) {
ASSERT_EQ(read_data[i].r, i);
ASSERT_EQ(read_data[i].i, i + 1);
}
ASSERT_EQ(pb.size(), 0);
}
TEST(overflow_buffer, push_zeros)
{
overflow_buffer pb(10);
pb.push_zeros(5);
ASSERT_EQ(pb.size(), 5);
cf_t data[5];
for (int i = 0; i < 5; i++) {
data[i] = {(float)i, (float)(i + 1)};
}
pb.push_samples(data, 5);
ASSERT_EQ(pb.size(), 10);
cf_t read_data[10];
ASSERT_EQ(pb.pop_samples(read_data, 10), 10);
for (int i = 0; i < 10; i++) {
if (i < 5) {
ASSERT_EQ(read_data[i].r, 0);
ASSERT_EQ(read_data[i].i, 0);
} else {
ASSERT_EQ(read_data[i].r, data[i - 5].r);
ASSERT_EQ(read_data[i].i, data[i - 5].i);
}
}
ASSERT_EQ(pb.size(), 0);
}
int main(int argc, char **argv)
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

View File

@@ -0,0 +1,162 @@
/*
* SPDX-License-Identifier: LicenseRef-CSSL-1.0
*/
#include "common_lib.h"
#include <gtest/gtest.h>
#include "common/config/config_userapi.h"
#include <zmq.h>
extern "C" {
#include "common/config/config_userapi.h"
#include "openair1/SIMULATION/TOOLS/sim.h"
extern int device_init(openair0_device_t *device, openair0_config_t *openair0_cfg);
static softmodem_params_t softmodem_params;
softmodem_params_t *get_softmodem_params(void)
{
return &softmodem_params;
}
}
#include "common/platform_types.h"
#include <thread>
configmodule_interface_t *uniqCfg = NULL;
extern "C" void exit_function(const char *file, const char *function, const int line, const char *s, const int assert)
{
fprintf(stderr, "FATAL: %s at %s:%s:%d\n", s, file, function, line);
exit(EXIT_FAILURE);
}
class ZMQTest : public ::testing::Test {
protected:
configmodule_interface_t *cfg1 = nullptr;
configmodule_interface_t *cfg2 = nullptr;
openair0_device_t device1 = {0};
openair0_device_t device2 = {0};
openair0_config_t config1 = {0};
openair0_config_t config2 = {0};
char *argv[4];
void SetUp() override
{
argv[0] = strdup("--zmq.[0].tx_channels");
argv[1] = strdup("tcp://127.0.0.1:5555");
argv[2] = strdup("--zmq.[0].rx_channels");
argv[3] = strdup("tcp://127.0.0.1:5556");
cfg1 = load_configmodule(sizeofArray(argv), argv, CONFIG_ENABLECMDLINEONLY);
uniqCfg = cfg1;
// 2. Initialize the ZMQ device
config1.tx_num_channels = 1;
config1.rx_num_channels = 1;
config1.sample_rate = 30;
ASSERT_EQ(device_init(&device1, &config1), 0);
ASSERT_EQ(device1.trx_start_func(&device1), 0);
// Swap the RX with TX for second device
char *tmp = argv[0];
argv[0] = argv[2];
argv[2] = tmp;
cfg2 = load_configmodule(sizeofArray(argv), argv, CONFIG_ENABLECMDLINEONLY);
uniqCfg = cfg2;
config2.tx_num_channels = 1;
config2.rx_num_channels = 1;
config2.sample_rate = 1500;
ASSERT_EQ(device_init(&device2, &config2), 0);
ASSERT_EQ(device2.trx_start_func(&device2), 0);
}
void TearDown() override
{
if (device1.trx_end_func) {
device1.trx_end_func(&device1);
}
if (device2.trx_end_func) {
device2.trx_end_func(&device2);
}
if (cfg1) {
end_configmodule(cfg1);
}
if (cfg2) {
end_configmodule(cfg2);
}
for (auto i = 0U; i < sizeofArray(argv); i++) {
free(argv[i]);
}
}
};
TEST_F(ZMQTest, RXSamples)
{
std::thread t1([this]() {
c16_t rx_samples[10];
openair0_timestamp_t rx_timestamp;
void *samples[1] = {rx_samples};
ASSERT_EQ(device1.trx_read_func(&device1, &rx_timestamp, samples, 10, 1), 10);
for (int i = 0; i < 10; i++) {
ASSERT_EQ(rx_samples[i].r, 0);
ASSERT_EQ(rx_samples[i].i, 0);
}
});
std::thread t2([this]() {
c16_t rx_samples[10];
openair0_timestamp_t rx_timestamp;
void *samples[1] = {rx_samples};
ASSERT_EQ(device2.trx_read_func(&device2, &rx_timestamp, samples, 10, 1), 10);
for (int i = 0; i < 10; i++) {
ASSERT_EQ(rx_samples[i].r, 0);
ASSERT_EQ(rx_samples[i].i, 0);
}
});
t1.join();
t2.join();
}
TEST_F(ZMQTest, TxRxSamples)
{
std::thread t1([this]() {
c16_t rx_samples[10];
openair0_timestamp_t rx_timestamp;
void *samples[1] = {rx_samples};
ASSERT_EQ(device1.trx_read_func(&device1, &rx_timestamp, samples, 10, 1), 10);
for (int i = 0; i < 10; i++) {
ASSERT_EQ(rx_samples[i].r, 0);
ASSERT_EQ(rx_samples[i].i, 0);
}
c16_t tx_samples[10];
openair0_timestamp_t tx_timestamp = rx_timestamp + 10;
for (int i = 0; i < 10; i++) {
tx_samples[i].r = i;
tx_samples[i].i = i + 1;
}
samples[0] = tx_samples;
ASSERT_EQ(device1.trx_write_func(&device1, tx_timestamp, samples, 10, 1, 0), 10);
});
std::thread t2([this]() {
c16_t rx_samples[10];
openair0_timestamp_t rx_timestamp;
void *samples[1] = {rx_samples};
ASSERT_EQ(device2.trx_read_func(&device2, &rx_timestamp, samples, 10, 1), 10);
for (int i = 0; i < 10; i++) {
ASSERT_EQ(rx_samples[i].r, 0);
ASSERT_EQ(rx_samples[i].i, 0);
}
openair0_timestamp_t rx_timestamp2;
ASSERT_EQ(device2.trx_read_func(&device2, &rx_timestamp2, samples, 10, 1), 10);
for (int i = 0; i < 10; i++) {
ASSERT_EQ(rx_samples[i].r, i);
ASSERT_EQ(rx_samples[i].i, i + 1);
}
ASSERT_EQ(rx_timestamp + 10, rx_timestamp2);
});
t1.join();
t2.join();
}
int main(int argc, char **argv)
{
logInit();
g_log->log_component[HW].level = OAILOG_DEBUG;
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

131
radio/zmq/zmq_imported.cpp Normal file
View File

@@ -0,0 +1,131 @@
/*
* SPDX-License-Identifier: BSD-3-Clause-Open-MPI
* Based on zmq library in OCUDU project: ocudu/lib/radio/zmq
* Refer to https://gitlab.com/ocudu/ocudu/-/raw/dev/LICENSE?ref_type=heads
*/
#include "zmq_imported.h"
#include "log.h"
const float c16_t_to_cf_t_factor = std::numeric_limits<int16_t>::max();
static constexpr std::chrono::milliseconds TRANSMIT_TS_ALIGN_TIMEOUT = std::chrono::milliseconds(0);
static constexpr std::chrono::milliseconds RECEIVE_TS_ALIGN_TIMEOUT = std::chrono::milliseconds(100);
void zmq_tx_channel::transmit(c16_t *samples, size_t nsamps, uint64_t timestamp)
{
std::scoped_lock lock(transmit_alignment_mutex_);
size_t overflow = 0;
if (timestamp > sample_count_) {
overflow += buffer_.push_zeros(timestamp - sample_count_);
sample_count_ = timestamp;
}
cf_t samples_float[nsamps];
for (size_t i = 0; i < nsamps; i++) {
samples_float[i].r = samples[i].r / c16_t_to_cf_t_factor;
samples_float[i].i = samples[i].i / c16_t_to_cf_t_factor;
}
overflow += buffer_.push_samples(samples_float, nsamps);
sample_count_ += nsamps;
if (overflow) {
LOG_W(HW, "Overflow on ZMQ channel by %lu samples\n", overflow);
}
is_tx_enabled_ = true;
transmit_alignment_cvar_.notify_all();
}
void zmq_tx_channel::start(uint64_t init_time)
{
sample_count_ = init_time;
}
bool zmq_tx_channel::align(uint64_t timestamp, std::chrono::milliseconds timeout)
{
if (sample_count_ >= timestamp) {
return sample_count_ > timestamp;
}
std::unique_lock<std::mutex> lock(transmit_alignment_mutex_);
if (is_tx_enabled_ && (timeout.count() != 0)) {
bool is_not_timeout =
transmit_alignment_cvar_.wait_for(lock, timeout, [this, timestamp]() { return sample_count_ >= timestamp; });
if (is_not_timeout) {
return sample_count_ > timestamp;
}
LOG_W(HW, "Timeout waiting for TX path to align samples\n");
is_tx_enabled_ = false;
}
if (sample_count_ < timestamp) {
buffer_.push_zeros(timestamp - sample_count_);
sample_count_ = timestamp;
}
return false;
}
void zmq_rx_channel::receive(c16_t *samples, size_t nsamps)
{
size_t samples_popped = 0;
cf_t samples_float[nsamps];
while (samples_popped < (size_t)nsamps && !stopped_) {
size_t popped_now = buffer_.pop_samples(samples_float + samples_popped, nsamps - samples_popped);
samples_popped += popped_now;
if (popped_now == 0) {
usleep(100); // wait for more samples to arrive
}
}
for (size_t i = 0; i < nsamps; i++) {
samples[i].r = samples_float[i].r * c16_t_to_cf_t_factor + 0.5;
samples[i].i = samples_float[i].i * c16_t_to_cf_t_factor + 0.5;
}
}
void zmq_rx_channel::stop()
{
stopped_ = true;
}
void zmq_tx_stream::start(uint64_t init_time)
{
for (auto &chan : channels_) {
chan->start(init_time);
}
}
bool zmq_tx_stream::align(uint64_t timestamp, std::chrono::milliseconds timeout)
{
bool timestamp_passed = false;
for (auto &chan : channels_) {
timestamp_passed = timestamp_passed || chan->align(timestamp, timeout);
}
return timestamp_passed;
}
void zmq_tx_stream::transmit(c16_t **samples, size_t nsamps, uint64_t timestamp)
{
bool timestamp_passed = align(timestamp, TRANSMIT_TS_ALIGN_TIMEOUT);
if (timestamp_passed) {
LOG_W(HW, "Error, channel timeout\n");
return;
}
int i = 0;
for (auto chan : channels_) {
chan->transmit(samples[i++], nsamps, timestamp);
}
}
void zmq_rx_stream::start(uint64_t init_time)
{
sample_count_ = init_time;
}
void zmq_rx_stream::stop()
{
for (auto &chan : channels_) {
chan->stop();
}
}
void zmq_rx_stream::receive(c16_t **samples, size_t nsamps, uint64_t *timestamp)
{
*timestamp = sample_count_;
uint64_t passed_timestamp = sample_count_ + nsamps;
tx_stream_->align(passed_timestamp, RECEIVE_TS_ALIGN_TIMEOUT);
int i = 0;
for (auto chan : channels_) {
chan->receive(samples[i++], nsamps);
}
sample_count_ += nsamps;
}

71
radio/zmq/zmq_imported.h Normal file
View File

@@ -0,0 +1,71 @@
/*
* SPDX-License-Identifier: BSD-3-Clause-Open-MPI
* Based on zmq library in OCUDU project: ocudu/lib/radio/zmq
* Refer to https://gitlab.com/ocudu/ocudu/-/raw/dev/LICENSE?ref_type=heads
*/
#ifndef ZMQ_IMPORTED_H
#define ZMQ_IMPORTED_H
#include <zmq.h>
#include "ring_buffer.h"
#include <condition_variable>
#include <atomic>
#include <mutex>
#include <vector>
class zmq_tx_channel {
public:
void *socket_;
overflow_buffer buffer_;
std::atomic<uint64_t> sample_count_ = 0;
std::atomic<bool> is_tx_enabled_ = false;
std::mutex transmit_alignment_mutex_;
std::condition_variable transmit_alignment_cvar_;
zmq_tx_channel(void *s, uint64_t buffer_size) : socket_(s), buffer_(buffer_size)
{
}
void transmit(c16_t *samples, size_t nsamps, uint64_t timestamp);
void start(uint64_t init_time);
bool align(uint64_t timestamp, std::chrono::milliseconds timeout);
};
class zmq_rx_channel {
public:
void *socket_;
overflow_buffer buffer_;
bool request_sent_;
std::atomic<bool> stopped_;
zmq_rx_channel(void *s, uint64_t buffer_size) : socket_(s), buffer_(buffer_size), stopped_(false)
{
}
void receive(c16_t *samples, size_t nsamps);
void stop();
};
class zmq_tx_stream {
public:
std::vector<zmq_tx_channel *> channels_;
void start(uint64_t init_time);
bool align(uint64_t timestamp, std::chrono::milliseconds timeout);
void transmit(c16_t **samples, size_t nsamps, uint64_t timestamp);
};
class zmq_rx_stream {
public:
std::vector<zmq_rx_channel *> channels_;
zmq_tx_stream *tx_stream_;
uint64_t sample_count_ = 0;
zmq_rx_stream() : sample_count_(0)
{
}
void start(uint64_t init_time);
void stop();
void receive(c16_t **samples, size_t nsamps, uint64_t *timestamp);
};
#endif

345
radio/zmq/zmq_radio.cpp Normal file
View File

@@ -0,0 +1,345 @@
/*
* SPDX-License-Identifier: LicenseRef-CSSL-1.0
*/
#include "PHY/TOOLS/tools_defs.h"
#include "PHY/defs_common.h"
#include "common/platform_types.h"
#include "softmodem-common.h"
#include "utils.h"
#include <chrono>
#include <cstdint>
#include <limits>
#include <stddef.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdbool.h>
#include <errno.h>
#include <sys/epoll.h>
#include <netdb.h>
#include <common/utils/assertions.h>
#include <common/utils/LOG/log.h>
#include <common/config/config_userapi.h>
#include "common_lib.h"
#include <queue>
#include <mutex>
#include <vector>
#include <sstream>
#include <algorithm>
#include <numeric>
#include <thread>
#include <atomic>
#include <condition_variable>
#include <ring_buffer.h>
#include <zmq.h>
#include "zmq_imported.h"
#define ZMQ_SECTION "zmq"
#define ZMQ_TX_CHANNELS "tx_channels"
#define ZMQ_RX_CHANNELS "rx_channels"
#define ZMQ_PARAMS_DESC \
{ \
STRINGLISTPARAM(ZMQ_TX_CHANNELS, "list of zmq addresses represeting tx channels_\n", PARAMFLAG_MANDATORY, nullptr, nullptr), \
STRINGLISTPARAM(ZMQ_RX_CHANNELS, "list of zmq addresses represeting rx channels_\n", PARAMFLAG_MANDATORY, nullptr, nullptr), \
};
const size_t sample_size = sizeof(cf_t);
const size_t rx_buffer_size = sample_size * 300000;
typedef struct {
void *context;
zmq_tx_stream tx_stream;
zmq_rx_stream rx_stream;
std::thread poll_thread;
std::atomic<bool> poll_thread_running;
bool stopped = false;
double sample_rate;
} zmq_state_t;
static void poll_thread(zmq_state_t *s)
{
s->poll_thread_running = true;
unsigned char *rx_buffer = static_cast<unsigned char *>(malloc(rx_buffer_size));
const auto num_tx_channels = s->tx_stream.channels_.size();
const auto num_rx_channels = s->rx_stream.channels_.size();
std::vector<zmq_pollitem_t> items(num_tx_channels + num_rx_channels);
std::vector<bool> reply_requested(num_tx_channels);
for (size_t i = 0; i < num_tx_channels; ++i) {
items[i] = {s->tx_stream.channels_[i]->socket_, 0, ZMQ_POLLIN, 0};
// wait for REQ
reply_requested[i] = false;
}
for (size_t i = 0; i < num_rx_channels; i++) {
items[i + num_tx_channels] = {s->rx_stream.channels_[i]->socket_, 0, ZMQ_POLLIN, 0};
}
const auto num_channels = num_tx_channels + num_rx_channels;
while (s->poll_thread_running) {
for (size_t i = 0; i < num_tx_channels; i++) {
auto chan = s->tx_stream.channels_[i];
if (!reply_requested[i]) {
continue;
}
std::vector<cf_t> samples(1024);
size_t num_popped = chan->buffer_.pop_samples(samples.data(), 1024);
if (num_popped == 0) {
continue;
}
int rc = zmq_send(chan->socket_, samples.data(), num_popped * sizeof(cf_t), 0);
if (rc < 0) {
LOG_E(HW, "[ZMQ] poll_thread zmq_send for TX antenna %d failed: %s\n", (int)i, zmq_strerror(errno));
}
reply_requested[i] = false;
}
int rc = zmq_poll(items.data(), num_channels, 10); // 10ms timeout
if (rc < 0) {
if (errno == EINTR)
continue;
LOG_E(HW, "[ZMQ] poll_thread zmq_poll failed: %s\n", zmq_strerror(errno));
break;
}
if (rc == 0) {
continue; // timeout
}
// --- TX Sockets (ZMQ_REP) ---
for (size_t i = 0; i < num_tx_channels; i++) {
if (items[i].revents & ZMQ_POLLIN) {
auto chan = s->tx_stream.channels_[i];
char dummy;
rc = zmq_recv(chan->socket_, &dummy, 1, 0);
if (rc < 0) {
LOG_E(HW, "[ZMQ] poll_thread zmq_recv for TX antenna %d failed: %s\n", (int)i, zmq_strerror(errno));
continue;
}
if (reply_requested[i]) {
LOG_E(HW, "[ZMQ] Error, unexpected REQ before REP on TX antenna %d\n", (int)i);
}
reply_requested[i] = true;
}
}
// --- RX Sockets (ZMQ_REQ) ---
for (size_t i = 0; i < num_rx_channels; i++) {
if (items[i + num_tx_channels].revents & ZMQ_POLLIN) {
auto chan = s->rx_stream.channels_[i];
rc = zmq_recv(chan->socket_, rx_buffer, rx_buffer_size, 0);
if (rc < 0) {
LOG_E(HW, "[ZMQ] poll_thread zmq_recv for RX antenna %d failed: %s\n", (int)i, zmq_strerror(errno));
} else {
size_t received_bytes = rc;
if (rx_buffer_size < received_bytes) {
LOG_W(HW,
"[ZMQ] the RX buffer is too small! The received message size is %lu while the buffer is %lu. Message truncated\n",
received_bytes,
rx_buffer_size);
}
size_t num_samples_received = std::min(received_bytes, rx_buffer_size) / sizeof(cf_t);
cf_t *samples = reinterpret_cast<cf_t *>(rx_buffer);
size_t overflow = chan->buffer_.push_samples(samples, num_samples_received);
if (rx_buffer_size < received_bytes) {
overflow += chan->buffer_.push_zeros((received_bytes - rx_buffer_size) / sizeof(cf_t));
}
if (overflow) {
LOG_W(HW, "Overflow on receive\n");
}
// After receiving, send next request to keep the stream flowing
char dummy = 0;
if (zmq_send(chan->socket_, &dummy, 1, 0) != 1) {
LOG_E(HW, "[ZMQ] poll_thread zmq_send for RX antenna %d failed: %s\n", (int)i, zmq_strerror(errno));
}
}
}
}
}
free(rx_buffer);
}
static int zmq_write(openair0_device_t *device, openair0_timestamp_t timestamp, void **buff, int nsamps, int cc, int flags)
{
zmq_state_t *s = static_cast<zmq_state_t *>(device->priv);
AssertFatal((uint)cc == s->tx_stream.channels_.size(),
"Request to write on more antennas (%d) than configured (%d)",
cc,
(int)s->tx_stream.channels_.size());
s->tx_stream.transmit((c16_t **)buff, nsamps, timestamp);
return nsamps;
}
static int zmq_read(openair0_device_t *device, openair0_timestamp_t *ptimestamp, void **samplesVoid, int nsamps, int nbAnt)
{
zmq_state_t *s = static_cast<zmq_state_t *>(device->priv);
AssertFatal((uint)nbAnt == s->rx_stream.channels_.size(),
"Request to read on more antennas (%d) than configured (%d)",
nbAnt,
(int)s->rx_stream.channels_.size());
uint64_t timestamp;
s->rx_stream.receive((c16_t **)samplesVoid, nsamps, &timestamp);
*ptimestamp = timestamp;
return nsamps;
}
static int zmq_get_stats(openair0_device_t *device)
{
return 0;
}
static int zmq_reset_stats(openair0_device_t *device)
{
return 0;
}
static void zmq_end(openair0_device_t *device)
{
zmq_state_t *s = static_cast<zmq_state_t *>(device->priv);
if (s) {
if (s->poll_thread_running) {
s->poll_thread_running = false;
if (s->poll_thread.joinable()) {
s->poll_thread.join();
}
}
for (auto &chan : s->tx_stream.channels_) {
if (chan->socket_)
zmq_close(chan->socket_);
delete chan;
}
s->tx_stream.channels_.clear();
for (auto &chan : s->rx_stream.channels_) {
if (chan->socket_)
zmq_close(chan->socket_);
delete chan;
}
s->rx_stream.channels_.clear();
if (s->context)
zmq_ctx_destroy(s->context);
delete s;
}
}
static int zmq_start(openair0_device_t *device)
{
zmq_state_t *s = static_cast<zmq_state_t *>(device->priv);
s->rx_stream.start(s->sample_rate / 100);
s->tx_stream.start(s->sample_rate / 100);
for (size_t i = 0; i < s->rx_stream.channels_.size(); i++) {
auto channel = s->rx_stream.channels_[i];
// Send initial request to start data flow
char dummy = 0;
if (zmq_send(channel->socket_, &dummy, 1, 0) != 1) {
LOG_E(HW, "[ZMQ] zmq_send for initial RX request failed for antenna %lu: %s\n", i, zmq_strerror(errno));
return -1;
}
}
s->poll_thread = std::thread(poll_thread, s);
return 0;
}
static int zmq_stop(openair0_device_t *device)
{
zmq_state_t *s = static_cast<zmq_state_t *>(device->priv);
s->rx_stream.stop();
return 0;
}
static int zmq_set_freq(openair0_device_t *device, openair0_config_t *openair0_cfg)
{
return 0;
}
static int zmq_set_gains(openair0_device_t *device, openair0_config_t *openair0_cfg)
{
return 0;
}
static int zmq_write_init(openair0_device_t *device)
{
return 0;
}
extern "C" __attribute__((__visibility__("default"))) int device_init(openair0_device_t *device, openair0_config_t *openair0_cfg)
{
auto *zmq_state = new zmq_state_t();
zmq_state->context = zmq_ctx_new();
AssertFatal(zmq_state->context != NULL, "zmq_ctx_new failed");
LOG_I(HW, "[ZMQ] tx_antennas: %d, rx_antennas: %d\n", openair0_cfg->tx_num_channels, openair0_cfg->rx_num_channels);
configmodule_interface_t *cfg = config_get_if();
paramdef_t param_desc[] = ZMQ_PARAMS_DESC;
std::string zmq_section = std::string(ZMQ_SECTION);
int ru_id = openair0_cfg->ru_id;
std::string zmq_array_section = std::string(ZMQ_SECTION) + ".[" + std::to_string(ru_id) + "]";
int ret = config_get(cfg, param_desc, sizeofArray(param_desc), zmq_array_section.c_str());
AssertFatal(ret >= 0, "configuration couldn't be performed\n");
int num_configured_tx_channels = gpd(param_desc, sizeofArray(param_desc), ZMQ_TX_CHANNELS)->numelt;
AssertFatal(num_configured_tx_channels == openair0_cfg->tx_num_channels,
"Incorrect configuration: Number of zmq tx channels (%d) != number of configured tx channels (%d)\n",
num_configured_tx_channels,
openair0_cfg->tx_num_channels);
int num_configured_rx_channels = gpd(param_desc, sizeofArray(param_desc), ZMQ_RX_CHANNELS)->numelt;
AssertFatal(num_configured_rx_channels == openair0_cfg->rx_num_channels,
"Incorrect configuration: Number of zmq rx channels (%d) != number of configured rx channels (%d)\n",
num_configured_rx_channels,
openair0_cfg->rx_num_channels);
char **tx_channels = gpd(param_desc, sizeofArray(param_desc), ZMQ_TX_CHANNELS)->strlistptr;
char **rx_channels = gpd(param_desc, sizeofArray(param_desc), ZMQ_RX_CHANNELS)->strlistptr;
// Setup TX sockets (one per antenna)
if (openair0_cfg->tx_num_channels > 0) {
zmq_state->tx_stream.channels_.resize(openair0_cfg->tx_num_channels);
for (int i = 0; i < openair0_cfg->tx_num_channels; i++) {
void *socket = zmq_socket(zmq_state->context, ZMQ_REP);
AssertFatal(socket != NULL, "zmq_socket(ZMQ_REP) for TX antenna %d failed", i);
int linger = 0;
zmq_setsockopt(socket, ZMQ_LINGER, &linger, sizeof(linger));
AssertFatal(zmq_bind(socket, tx_channels[i]) == 0, "zmq_bind for TX antenna %d failed on %s", i, tx_channels[i]);
auto channel = new zmq_tx_channel(socket, openair0_cfg->sample_rate);
LOG_I(HW, "[ZMQ] TX socket for antenna %d bound to %s\n", i, tx_channels[i]);
zmq_state->tx_stream.channels_[i] = channel;
}
}
zmq_state->sample_rate = openair0_cfg->sample_rate;
// Setup RX sockets (one per antenna)
if (openair0_cfg->rx_num_channels > 0) {
zmq_state->rx_stream.channels_.resize(openair0_cfg->rx_num_channels);
for (int i = 0; i < openair0_cfg->rx_num_channels; i++) {
void *socket = zmq_socket(zmq_state->context, ZMQ_REQ);
AssertFatal(socket != NULL, "zmq_socket(ZMQ_REQ) for RX antenna %d failed", i);
int linger = 0;
zmq_setsockopt(socket, ZMQ_LINGER, &linger, sizeof(linger));
AssertFatal(zmq_connect(socket, rx_channels[i]) == 0, "zmq_connect for RX antenna %d failed on %s", i, rx_channels[i]);
auto channel = new zmq_rx_channel(socket, openair0_cfg->sample_rate);
LOG_I(HW, "[ZMQ] RX socket for antenna %d connected to %s\n", i, rx_channels[i]);
zmq_state->rx_stream.channels_[i] = channel;
}
zmq_state->rx_stream.tx_stream_ = &zmq_state->tx_stream;
}
device->trx_start_func = zmq_start;
device->trx_get_stats_func = zmq_get_stats;
device->trx_reset_stats_func = zmq_reset_stats;
device->trx_end_func = zmq_end;
device->trx_stop_func = zmq_stop;
device->trx_set_freq_func = zmq_set_freq;
device->trx_set_gains_func = zmq_set_gains;
device->trx_write_func = zmq_write;
device->trx_read_func = zmq_read;
device->type = RFSIMULATOR;
IS_SOFTMODEM_RFSIM = 1U;
openair0_cfg->rx_gain[0] = 0;
device->openair0_cfg = openair0_cfg;
device->priv = zmq_state;
device->trx_write_init = zmq_write_init;
return 0;
}