Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3117df91cb | ||
|
|
38851239a2 | ||
|
|
9159bf319f | ||
|
|
2244eb8e99 | ||
|
|
817e04cc29 | ||
|
|
3e58ab0d3a | ||
|
|
dfcd974501 | ||
|
|
5874aa1fe7 | ||
|
|
312d75d759 | ||
|
|
90290d0d7b |
@@ -873,6 +873,7 @@ set(PHY_SRC_RU
|
||||
${OPENAIR1_DIR}/PHY/MODULATION/gen_75KHz.cpp
|
||||
${OPENAIR1_DIR}/PHY/MODULATION/beamforming.c
|
||||
${OPENAIR1_DIR}/PHY/MODULATION/compute_bf_weights.c
|
||||
${OPENAIR1_DIR}/PHY/NR_TRANSPORT/nr_antenna_state.c
|
||||
${OPENAIR1_DIR}/PHY/INIT/lte_init_ru.c
|
||||
${OPENAIR1_DIR}/PHY/INIT/nr_init_ru.c
|
||||
${OPENAIR1_DIR}/PHY/LTE_TRANSPORT/prach.c
|
||||
@@ -948,6 +949,7 @@ set(PHY_SRC_UE
|
||||
${OPENAIR1_DIR}/PHY/NR_TRANSPORT/nr_prach.c
|
||||
${OPENAIR1_DIR}/PHY/NR_TRANSPORT/nr_ulsch_llr_computation.c
|
||||
${OPENAIR1_DIR}/PHY/NR_TRANSPORT/nr_ulsch_demodulation.c
|
||||
${OPENAIR1_DIR}/PHY/NR_TRANSPORT/nr_antenna_control.c
|
||||
${OPENAIR1_DIR}/PHY/NR_REFSIG/ul_ref_seq_nr.c
|
||||
${OPENAIR1_DIR}/PHY/NR_REFSIG/nr_dmrs_rx.c
|
||||
${OPENAIR1_DIR}/PHY/NR_REFSIG/nr_gen_mod_table.c
|
||||
|
||||
@@ -11,8 +11,7 @@ gNBs =
|
||||
|
||||
// Tracking area code, 0x0000 and 0xfffe are reserved values
|
||||
tracking_area_code = 1;
|
||||
plmn_list = ({ mcc = 222; mnc = 01; mnc_length = 2; snssaiList = ({ sst = 1, sd = 0xffffff }) });
|
||||
|
||||
plmn_list = ({ mcc = 001; mnc = 01; mnc_length = 2; snssaiList = ({ sst = 1; sd = 0xffffff}) });
|
||||
nr_cellid = 12345678L;
|
||||
|
||||
////////// Physical parameters:
|
||||
@@ -188,8 +187,8 @@ L1s = (
|
||||
RUs = (
|
||||
{
|
||||
local_rf = "yes"
|
||||
nb_tx = 1
|
||||
nb_rx = 1
|
||||
nb_tx = 4;
|
||||
nb_rx = 4;
|
||||
att_tx = 14;
|
||||
att_rx = 14;
|
||||
bands = [78];
|
||||
@@ -200,6 +199,14 @@ RUs = (
|
||||
}
|
||||
);
|
||||
|
||||
rfsimulator :
|
||||
{
|
||||
serveraddr = "server";
|
||||
serverport = "4043";
|
||||
# options = ("saviq", "chanmod"); #("saviq"); or/and "chanmod"
|
||||
# modelname = "AWGN";
|
||||
IQfile = "/tmp/rfsimulator.iqs";
|
||||
};
|
||||
|
||||
security = {
|
||||
# preferred ciphering algorithms
|
||||
|
||||
@@ -14,6 +14,13 @@ position0 = {
|
||||
|
||||
thread-pool = "-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1"
|
||||
|
||||
rfsimulator :
|
||||
{
|
||||
serveraddr = "192.168.71.140"; // gNB IP address
|
||||
serverport = 4043;
|
||||
options = ("chanmod"); // Enable channel modeling
|
||||
};
|
||||
|
||||
#/* configuration for channel modelisation */
|
||||
#/* To be included in main config file when */
|
||||
#/* channel modelisation is used (rfsimulator with chanmod options enabled) */
|
||||
@@ -25,7 +32,7 @@ channelmod = {
|
||||
model_name = "rfsimu_channel_enB0"
|
||||
type = "AWGN";
|
||||
ploss_dB = 20;
|
||||
noise_power_dB = -4;
|
||||
noise_power_dB = -2;
|
||||
forgetfact = 0;
|
||||
offset = 0;
|
||||
ds_tdl = 0;
|
||||
@@ -33,8 +40,8 @@ channelmod = {
|
||||
{ # UL, modify on gNB side
|
||||
model_name = "rfsimu_channel_ue0"
|
||||
type = "AWGN";
|
||||
ploss_dB = 20;
|
||||
noise_power_dB = -2;
|
||||
ploss_dB = 0;
|
||||
noise_power_dB = -100;
|
||||
forgetfact = 0;
|
||||
offset = 0;
|
||||
ds_tdl = 0;
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
Active_gNBs = ( "gNB-OAI");
|
||||
# Asn1_verbosity, choice in: none, info, annoying
|
||||
Asn1_verbosity = "none";
|
||||
|
||||
gNBs =
|
||||
(
|
||||
{
|
||||
////////// Identification parameters:
|
||||
gNB_ID = 0xe00;
|
||||
gNB_name = "gNB-OAI";
|
||||
|
||||
// Tracking area code, 0x0000 and 0xfffe are reserved values
|
||||
tracking_area_code = 1;
|
||||
plmn_list = ({ mcc = 001; mnc = 01; mnc_length = 2; snssaiList = ({ sst = 1; }) });
|
||||
|
||||
nr_cellid = 1;
|
||||
|
||||
////////// Physical parameters:
|
||||
|
||||
pdsch_AntennaPorts_XP = 2;
|
||||
pdsch_AntennaPorts_N1 = 1;
|
||||
maxMIMO_layers = 2;
|
||||
pusch_AntennaPorts = 4;
|
||||
do_CSIRS = 1;
|
||||
do_SRS = 0;
|
||||
# force_UL256qam_off = 1;
|
||||
|
||||
servingCellConfigCommon = (
|
||||
{
|
||||
#spCellConfigCommon
|
||||
|
||||
physCellId = 0;
|
||||
# n_TimingAdvanceOffset = 0;
|
||||
# downlinkConfigCommon
|
||||
#frequencyInfoDL
|
||||
# center frequency = 3350.01 MHz
|
||||
# selected SSB frequency = 3349.92 MHz
|
||||
absoluteFrequencySSB = 623328;
|
||||
dl_frequencyBand = 78;
|
||||
# frequency point A = 3300.87 MHz
|
||||
dl_absoluteFrequencyPointA = 620052;
|
||||
#scs-SpecificCarrierList
|
||||
dl_offstToCarrier = 0;
|
||||
# subcarrierSpacing
|
||||
# 0=kHz15, 1=kHz30, 2=kHz60, 3=kHz120
|
||||
dl_subcarrierSpacing = 1;
|
||||
dl_carrierBandwidth = 273;
|
||||
#initialDownlinkBWP
|
||||
#genericParameters
|
||||
initialDLBWPlocationAndBandwidth = 1099; #38.101-1 Table 5.3.2-1
|
||||
#
|
||||
# subcarrierSpacing
|
||||
# 0=kHz15, 1=kHz30, 2=kHz60, 3=kHz120
|
||||
initialDLBWPsubcarrierSpacing = 1;
|
||||
#pdcch-ConfigCommon
|
||||
initialDLBWPcontrolResourceSetZero = 11;
|
||||
initialDLBWPsearchSpaceZero = 0;
|
||||
|
||||
#uplinkConfigCommon
|
||||
#frequencyInfoUL
|
||||
ul_frequencyBand = 78;
|
||||
#scs-SpecificCarrierList
|
||||
ul_offstToCarrier = 0;
|
||||
# subcarrierSpacing
|
||||
# 0=kHz15, 1=kHz30, 2=kHz60, 3=kHz120
|
||||
ul_subcarrierSpacing = 1;
|
||||
ul_carrierBandwidth = 273;
|
||||
pMax = 23;
|
||||
#initialUplinkBWP
|
||||
#genericParameters
|
||||
initialULBWPlocationAndBandwidth = 1099;
|
||||
# subcarrierSpacing
|
||||
# 0=kHz15, 1=kHz30, 2=kHz60, 3=kHz120
|
||||
initialULBWPsubcarrierSpacing = 1;
|
||||
#rach-ConfigCommon
|
||||
#rach-ConfigGeneric
|
||||
prach_ConfigurationIndex = 152;
|
||||
#prach_msg1_FDM
|
||||
#0 = one, 1=two, 2=four, 3=eight
|
||||
prach_msg1_FDM = 0;
|
||||
prach_msg1_FrequencyStart = 0;
|
||||
zeroCorrelationZoneConfig = 0;
|
||||
preambleReceivedTargetPower = -100;
|
||||
#preamblTransMax (0...10) = (3,4,5,6,7,8,10,20,50,100,200)
|
||||
preambleTransMax = 8;
|
||||
#powerRampingStep
|
||||
# 0=dB0,1=dB2,2=dB4,3=dB6
|
||||
powerRampingStep = 1;
|
||||
#ssb_perRACH_OccasionAndCB_PreamblesPerSSB_PR
|
||||
#1=oneeighth,2=onefourth,3=half,4=one,5=two,6=four,7=eight,8=sixteen
|
||||
ssb_perRACH_OccasionAndCB_PreamblesPerSSB_PR = 4;
|
||||
#one (0..15) 4,8,12,16,...60,64
|
||||
ssb_perRACH_OccasionAndCB_PreamblesPerSSB = 15;
|
||||
#ra_ContentionResolutionTimer
|
||||
#(0..7) 8,16,24,32,40,48,56,64
|
||||
ra_ContentionResolutionTimer = 7;
|
||||
rsrp_ThresholdSSB = 19;
|
||||
#prach-RootSequenceIndex_PR
|
||||
#1 = 839, 2 = 139
|
||||
prach_RootSequenceIndex_PR = 2;
|
||||
prach_RootSequenceIndex = 1;
|
||||
# SCS for msg1, can only be 15 for 30 kHz < 6 GHz, takes precendence over the one derived from prach-ConfigIndex
|
||||
#
|
||||
msg1_SubcarrierSpacing = 1,
|
||||
# restrictedSetConfig
|
||||
# 0=unrestricted, 1=restricted type A, 2=restricted type B
|
||||
restrictedSetConfig = 0,
|
||||
|
||||
# this is the offset between the last PRACH preamble power and the Msg3 PUSCH, 2 times the field value in dB
|
||||
msg3_DeltaPreamble = 2;
|
||||
p0_NominalWithGrant = -96;
|
||||
|
||||
# pucch-ConfigCommon setup :
|
||||
# pucchGroupHopping
|
||||
# 0 = neither, 1= group hopping, 2=sequence hopping
|
||||
pucchGroupHopping = 0;
|
||||
hoppingId = 0;
|
||||
p0_nominal = -96;
|
||||
|
||||
ssb_PositionsInBurst_Bitmap = 0x1;
|
||||
|
||||
# ssb_periodicityServingCell
|
||||
# 0 = ms5, 1=ms10, 2=ms20, 3=ms40, 4=ms80, 5=ms160, 6=spare2, 7=spare1
|
||||
ssb_periodicityServingCell = 2;
|
||||
|
||||
# dmrs_TypeA_position
|
||||
# 0 = pos2, 1 = pos3
|
||||
dmrs_TypeA_Position = 0;
|
||||
|
||||
# subcarrierSpacing
|
||||
# 0=kHz15, 1=kHz30, 2=kHz60, 3=kHz120
|
||||
subcarrierSpacing = 1;
|
||||
|
||||
|
||||
#tdd-UL-DL-ConfigurationCommon
|
||||
# subcarrierSpacing
|
||||
# 0=kHz15, 1=kHz30, 2=kHz60, 3=kHz120
|
||||
referenceSubcarrierSpacing = 1;
|
||||
# pattern1
|
||||
# dl_UL_TransmissionPeriodicity
|
||||
# 0=ms0p5, 1=ms0p625, 2=ms1, 3=ms1p25, 4=ms2, 5=ms2p5, 6=ms5, 7=ms10
|
||||
dl_UL_TransmissionPeriodicity = 6;
|
||||
nrofDownlinkSlots = 7;
|
||||
nrofDownlinkSymbols = 6;
|
||||
nrofUplinkSlots = 2;
|
||||
nrofUplinkSymbols = 4;
|
||||
|
||||
ssPBCH_BlockPower = -15;
|
||||
}
|
||||
|
||||
);
|
||||
|
||||
|
||||
# ------- SCTP definitions
|
||||
SCTP :
|
||||
{
|
||||
# Number of streams to use in input/output
|
||||
SCTP_INSTREAMS = 2;
|
||||
SCTP_OUTSTREAMS = 2;
|
||||
};
|
||||
|
||||
|
||||
////////// AMF parameters:
|
||||
amf_ip_address = ({ ipv4 = "192.168.71.132"; });
|
||||
|
||||
NETWORK_INTERFACES :
|
||||
{
|
||||
GNB_IPV4_ADDRESS_FOR_NG_AMF = "192.168.71.140";
|
||||
GNB_IPV4_ADDRESS_FOR_NGU = "192.168.71.140";
|
||||
GNB_PORT_FOR_S1U = 2152; # Spec 2152
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
MACRLCs = (
|
||||
{
|
||||
num_cc = 1;
|
||||
tr_s_preference = "local_L1";
|
||||
tr_n_preference = "local_RRC";
|
||||
pusch_TargetSNRx10 = 100;
|
||||
pucch_TargetSNRx10 = 200;
|
||||
dl_bler_target_upper = .35;
|
||||
dl_bler_target_lower = .15;
|
||||
ul_bler_target_upper = .35;
|
||||
ul_bler_target_lower = .15;
|
||||
pusch_FailureThres = 1000;
|
||||
ul_max_mcs = 28;
|
||||
}
|
||||
);
|
||||
|
||||
L1s = (
|
||||
{
|
||||
num_cc = 1;
|
||||
tr_n_preference = "local_mac";
|
||||
prach_dtx_threshold = 90;
|
||||
pucch0_dtx_threshold = 80;
|
||||
pusch_dtx_threshold = 10;
|
||||
max_ldpc_iterations = 15;
|
||||
tx_amp_backoff_dB = 12; # needs to match O-RU configuration
|
||||
L1_rx_thread_core = 3;
|
||||
L1_tx_thread_core = 4;
|
||||
phase_compensation = 0; # needs to match O-RU configuration
|
||||
}
|
||||
);
|
||||
|
||||
RUs = (
|
||||
{
|
||||
local_rf = "yes";
|
||||
nb_tx = 4;
|
||||
nb_rx = 4;
|
||||
att_tx = 0;
|
||||
att_rx = 0;
|
||||
bands = [78];
|
||||
max_pdschReferenceSignalPower = -27;
|
||||
max_rxgain = 75;
|
||||
sf_extension = 0;
|
||||
eNB_instances = [0];
|
||||
ru_thread_core = 5;
|
||||
sl_ahead = 10;
|
||||
tr_preference = "raw_if4p5"; # important: activate FHI7.2
|
||||
do_precoding = 0; # needs to match O-RU configuration
|
||||
}
|
||||
);
|
||||
|
||||
security = {
|
||||
# preferred ciphering algorithms
|
||||
# the first one of the list that an UE supports in chosen
|
||||
# valid values: nea0, nea1, nea2, nea3
|
||||
ciphering_algorithms = ( "nea0" );
|
||||
|
||||
# preferred integrity algorithms
|
||||
# the first one of the list that an UE supports in chosen
|
||||
# valid values: nia0, nia1, nia2, nia3
|
||||
integrity_algorithms = ( "nia2", "nia0" );
|
||||
|
||||
# setting 'drb_ciphering' to "no" disables ciphering for DRBs, no matter
|
||||
# what 'ciphering_algorithms' configures; same thing for 'drb_integrity'
|
||||
drb_ciphering = "yes";
|
||||
drb_integrity = "no";
|
||||
};
|
||||
|
||||
log_config :
|
||||
{
|
||||
global_log_level = "info";
|
||||
hw_log_level = "info";
|
||||
phy_log_level = "info";
|
||||
mac_log_level = "info";
|
||||
rlc_log_level = "info";
|
||||
pdcp_log_level = "info";
|
||||
rrc_log_level = "info";
|
||||
ngap_log_level = "info";
|
||||
f1ap_log_level = "info";
|
||||
};
|
||||
|
||||
rfsimulator :
|
||||
{
|
||||
serveraddr = "server";
|
||||
serverport = 4043;
|
||||
options = (); #("saviq"); or/and "chanmod"
|
||||
modelname = "AWGN";
|
||||
IQfile = "/tmp/rfsimulator.iqs";
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
FROM ran-base:latest AS ran-build
|
||||
ARG BUILD_OPTION
|
||||
|
||||
RUN rm -Rf /oai-ran
|
||||
WORKDIR /oai-ran
|
||||
COPY . .
|
||||
|
||||
# build all targets so final targets can be created afterwards
|
||||
RUN /bin/sh oaienv && \
|
||||
cd cmake_targets && \
|
||||
./build_oai --ninja \
|
||||
--gNB --RU --nrUE -w SIMU -w USRP --build-lib telnetsrv \
|
||||
--build-e2 --cmake-opt -DKPM_VERSION=KPM_V3_00 --cmake-opt -DE2AP_VERSION=E2AP_V3 \
|
||||
--cmake-opt -DCMAKE_C_FLAGS="-Werror" --cmake-opt -DCMAKE_CXX_FLAGS="-Werror" $BUILD_OPTION && \
|
||||
echo "---- ldd on executables ----" && \
|
||||
ldd ran_build/build/*softmodem* ran_build/build/nr-cuup ran_build/build/oairu && \
|
||||
echo "---- ldd on shared libraries ----" && \
|
||||
ldd ran_build/build/*.so
|
||||
|
||||
RUN set -eux; \
|
||||
apt-get update; \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
ca-certificates curl wget iproute2 iputils-ping dnsutils jq \
|
||||
iperf iperf3 ffmpeg gdb\
|
||||
nghttp2-client || true; \
|
||||
if ! command -v h2load >/dev/null 2>&1; then \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends nghttp2 || true; \
|
||||
fi; \
|
||||
wget -O /usr/local/bin/sipp https://github.com/SIPp/sipp/releases/download/v3.7.7/sipp && chmod +x /usr/local/bin/sipp; \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# FlexRIC libraries (built by --build-e2) - only copy service model libraries (*_sm.so)
|
||||
# Also rebuild xapp_nakaolab (not built by default in build_oai)
|
||||
RUN mkdir -p /usr/local/etc/flexric /usr/local/lib/flexric && \
|
||||
find /oai-ran/openair2/E2AP/flexric/build -name "*_sm.so" -exec cp {} /usr/local/lib/flexric/ \; && \
|
||||
cp /oai-ran/openair2/E2AP/flexric/flexric.conf /usr/local/etc/flexric/ 2>/dev/null || true && \
|
||||
echo "/usr/local/lib/flexric" > /etc/ld.so.conf.d/flexric.conf && ldconfig && \
|
||||
cd /oai-ran/openair2/E2AP/flexric/build && \
|
||||
sed -i 's|/home/[^/]*/openairinterface5g|/oai-ran|g' CMakeCache.txt && \
|
||||
find . -name 'Makefile' -o -name '*.cmake' | xargs sed -i 's|/home/[^/]*/openairinterface5g|/oai-ran|g' 2>/dev/null || true && \
|
||||
rm -f examples/xApp/c/monitor/CMakeFiles/xapp_nakaolab.dir/xapp_nakaolab.c.o && \
|
||||
cmake --build . --target xapp_nakaolab 2>&1 | tail -5 && \
|
||||
ls -la /usr/local/lib/flexric/
|
||||
|
||||
# Install netcat for antenna control testing + paramiko for M-Plane NETCONF server
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
netcat-openbsd python3-pip \
|
||||
&& pip3 install --break-system-packages paramiko \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# O-RU M-Plane NETCONF server (o-ran-uplane-conf antenna mask control)
|
||||
# Listens on port 831, translates NETCONF edit-config to internal antenna control
|
||||
COPY mplane/mplane_server.py /opt/mplane/mplane_server.py
|
||||
COPY mplane/wg4-capability.json /opt/mplane/wg4-capability.json
|
||||
COPY mplane/gnb-mplane-entrypoint.sh /opt/mplane/gnb-mplane-entrypoint.sh
|
||||
RUN chmod +x /opt/mplane/gnb-mplane-entrypoint.sh
|
||||
|
||||
EXPOSE 831
|
||||
|
||||
ENTRYPOINT ["/bin/bash"]
|
||||
@@ -48,6 +48,8 @@
|
||||
#include "NR_PHY_INTERFACE/NR_IF_Module.h"
|
||||
#include "PHY/INIT/nr_phy_init.h"
|
||||
#include "PHY/MODULATION/nr_modulation.h"
|
||||
#include "openair2/LAYER2/NR_MAC_gNB/nr_mac_gNB.h"
|
||||
#include "PHY/NR_TRANSPORT/nr_antenna_control.h"
|
||||
#include "PHY/NR_TRANSPORT/nr_transport_proto.h"
|
||||
#include "PHY/TOOLS/tools_defs.h"
|
||||
#include "PHY/defs_RU.h"
|
||||
@@ -402,6 +404,14 @@ void init_eNB_afterRU(void)
|
||||
*/
|
||||
//init_precoding_weights(RC.gNB[inst]);
|
||||
init_gNB_Tpool(inst);
|
||||
|
||||
// antenna control
|
||||
if (nr_antenna_control_start_test_server(&gNB->antenna_state, 9999))
|
||||
LOG_I(PHY, "[ANTENNA_CTRL] Test control server started on port 9999\n");
|
||||
if (inst < RC.nb_nr_macrlc_inst && RC.nrmac && RC.nrmac[inst]) {
|
||||
RC.nrmac[inst]->antenna_state = &gNB->antenna_state;
|
||||
LOG_I(PHY, "[ANTENNA_STATE] Connected MAC scheduler to PHY antenna state for dynamic layer adaptation\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
# gNB entrypoint with M-Plane NETCONF server
|
||||
#
|
||||
# Starts the M-Plane server alongside the gNB process.
|
||||
# The M-Plane server acts as an O-RU NETCONF endpoint,
|
||||
# translating o-ran-uplane-conf edit-config to internal antenna control.
|
||||
#
|
||||
# Usage: gnb-mplane-entrypoint.sh <gnb-command-args...>
|
||||
# Example: gnb-mplane-entrypoint.sh /oai-ran/.../nr-softmodem -O /opt/.../gnb.conf --rfsim
|
||||
|
||||
set -e
|
||||
|
||||
echo "[gnb-mplane] Starting M-Plane NETCONF server (port ${MPLANE_PORT:-831})..."
|
||||
python3 /opt/mplane/mplane_server.py &
|
||||
MPLANE_PID=$!
|
||||
|
||||
# Give the M-Plane server a moment to start
|
||||
sleep 1
|
||||
|
||||
if ! kill -0 $MPLANE_PID 2>/dev/null; then
|
||||
echo "[gnb-mplane] WARNING: M-Plane server failed to start, continuing without it"
|
||||
fi
|
||||
|
||||
# Run the gNB command (passed as arguments)
|
||||
echo "[gnb-mplane] Starting gNB: $@"
|
||||
exec "$@"
|
||||
@@ -0,0 +1,602 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
O-RU M-Plane NETCONF Server for gNB Antenna Control
|
||||
|
||||
Lightweight NETCONF/SSH server implementing the O-RAN WG4 M-Plane interface
|
||||
for antenna mask configuration (o-ran-uplane-conf.yang).
|
||||
|
||||
The gNB acts as an O-RU for antenna control purposes:
|
||||
- Advertises mplane-supported-trx-control-masks (capability report)
|
||||
- Accepts tx-array-antenna-mask-config via NETCONF edit-config
|
||||
- Translates received mask to internal antenna control (localhost:9999)
|
||||
- Sends mplane-trx-control-ant-mask-update notification (logged)
|
||||
|
||||
Protocol:
|
||||
SSH transport (paramiko) -> NETCONF base:1.0 framing -> XML RPC processing
|
||||
|
||||
Architecture:
|
||||
o1-adapter (O-DU) --NETCONF:831--> mplane_server.py (O-RU) --localhost:9999--> nr_antenna_control.c
|
||||
"""
|
||||
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
import paramiko
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Configuration
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
MPLANE_PORT = 831
|
||||
MPLANE_USER = MPLANE_PASS = "netconf"
|
||||
GNB_CTRL_HOST = "127.0.0.1"
|
||||
GNB_CTRL_PORT = 9999
|
||||
WG4_CAPABILITY_FILE = "/opt/mplane/wg4-capability.json"
|
||||
SSH_HOST_KEY = "/opt/mplane/ssh_host_rsa_key"
|
||||
|
||||
NS_NC = "urn:ietf:params:xml:ns:netconf:base:1.0"
|
||||
NS_UPLANE = "urn:o-ran:uplane-conf:1.0"
|
||||
NETCONF_END = "]]>]]>" # https://www.rfc-editor.org/rfc/rfc4742 NETCONF base:1.0 message delimiter
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
stream=sys.stdout,
|
||||
)
|
||||
log = logging.getLogger("mplane-server")
|
||||
|
||||
# WG4 Capability (O-RU supported masks)
|
||||
def b64_to_mask(b64_str):
|
||||
return int.from_bytes(base64.b64decode(b64_str), byteorder="big")
|
||||
|
||||
def mask_to_b64(mask_int):
|
||||
byte_len = max(1, (mask_int.bit_length() + 7) // 8)
|
||||
return base64.b64encode(mask_int.to_bytes(byte_len, byteorder="big")).decode()
|
||||
|
||||
|
||||
class Capability:
|
||||
"""O-RU capability report: tx-arrays with supported masks."""
|
||||
|
||||
def __init__(self):
|
||||
self.tx_arrays = {} # name -> {"masks": [{index, b64, int}], ...}
|
||||
self.current_config = {} # array-name -> {"index": i, "mask_int": m}
|
||||
|
||||
def load(self, path):
|
||||
if not (path and os.path.exists(path)):
|
||||
# Fallback: generate for 4 antennas
|
||||
masks = []
|
||||
for n in range(4, 0, -1):
|
||||
m = (1 << n) - 1
|
||||
masks.append({"index": 4 - n, "b64": mask_to_b64(m), "int": m})
|
||||
self.tx_arrays["tx-array-0"] = {"masks": masks, "columns": 4, "layers": 4}
|
||||
self.current_config["tx-array-0"] = {"index": 0, "mask_int": 0x0F}
|
||||
log.info("Generated fallback capability (4 antennas)")
|
||||
return
|
||||
|
||||
with open(path) as f:
|
||||
data = json.load(f) # returns a dict
|
||||
for arr in data.get("tx-arrays", []): # blank list for fallback
|
||||
name = arr["name"]
|
||||
cap = arr.get("mplane-trx-control-txarr-capability-info", {})
|
||||
masks = []
|
||||
for m in cap.get("mplane-supported-trx-control-masks", []):
|
||||
masks.append({
|
||||
"index": m["index"],
|
||||
"b64": m["antenna-mask"],
|
||||
"int": b64_to_mask(m["antenna-mask"]),
|
||||
})
|
||||
self.tx_arrays[name] = {
|
||||
"masks": masks,
|
||||
"columns": arr.get("number-of-columns", 4),
|
||||
"layers": arr.get("number-of-array-layers", 4),
|
||||
}
|
||||
# Default: all active (index 0)
|
||||
if masks:
|
||||
self.current_config[name] = {
|
||||
"index": masks[0]["index"],
|
||||
"mask_int": masks[0]["int"],
|
||||
}
|
||||
log.info("Loaded capability: %d tx-arrays from %s", len(self.tx_arrays), path)
|
||||
|
||||
def resolve_index(self, array_name, index):
|
||||
arr = self.tx_arrays.get(array_name)
|
||||
if not arr:
|
||||
return None, f"unknown tx-array '{array_name}'"
|
||||
for m in arr["masks"]:
|
||||
if m["index"] == index:
|
||||
return m, ""
|
||||
return None, f"index {index} not in supported masks"
|
||||
|
||||
def resolve_mask(self, array_name, mask_int):
|
||||
arr = self.tx_arrays.get(array_name)
|
||||
if not arr:
|
||||
return None, f"unknown tx-array '{array_name}'"
|
||||
for m in arr["masks"]:
|
||||
if m["int"] == mask_int:
|
||||
return m, ""
|
||||
return None, f"mask 0x{mask_int:02X} not in supported masks"
|
||||
|
||||
def get_capability_xml(self):
|
||||
"""Generate o-ran-uplane-conf operational data XML for <get> response."""
|
||||
parts = [f'<user-plane-configuration xmlns="{NS_UPLANE}">']
|
||||
for name, arr in self.tx_arrays.items():
|
||||
parts.append(f"<tx-arrays><name>{name}</name>")
|
||||
parts.append(f"<number-of-columns>{arr['columns']}</number-of-columns>")
|
||||
parts.append(f"<number-of-array-layers>{arr['layers']}</number-of-array-layers>")
|
||||
parts.append("<mplane-trx-control-txarr-capability-info>")
|
||||
for m in arr["masks"]:
|
||||
parts.append("<mplane-supported-trx-control-masks>")
|
||||
parts.append(f"<index>{m['index']}</index>")
|
||||
parts.append(f"<antenna-mask>{m['b64']}</antenna-mask>")
|
||||
parts.append("</mplane-supported-trx-control-masks>")
|
||||
parts.append("</mplane-trx-control-txarr-capability-info>")
|
||||
parts.append("</tx-arrays>")
|
||||
# Current config
|
||||
for aname, cfg in self.current_config.items():
|
||||
parts.append("<tx-array-antenna-mask-config>")
|
||||
parts.append(f"<array-name>{aname}</array-name>")
|
||||
parts.append(f"<antenna-bitmask-index>{cfg['index']}</antenna-bitmask-index>")
|
||||
parts.append("</tx-array-antenna-mask-config>")
|
||||
parts.append("</user-plane-configuration>")
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# gNB internal antenna control client (localhost:9999)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class GnbCtrlClient:
|
||||
"""Connects to the gNB internal test socket to apply antenna masks."""
|
||||
|
||||
def __init__(self, host, port):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.sock = None
|
||||
|
||||
def connect(self):
|
||||
for attempt in range(30):
|
||||
try:
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.sock.settimeout(5.0)
|
||||
self.sock.connect((self.host, self.port))
|
||||
self._recv() # consume welcome
|
||||
log.info("Connected to gNB control at %s:%d", self.host, self.port)
|
||||
return True
|
||||
except (socket.error, ConnectionRefusedError) as e:
|
||||
log.debug("gNB ctrl connect attempt %d/30: %s", attempt + 1, e)
|
||||
if self.sock:
|
||||
self.sock.close()
|
||||
self.sock = None
|
||||
time.sleep(2)
|
||||
log.warning("Cannot connect to gNB control at %s:%d", self.host, self.port)
|
||||
return False
|
||||
|
||||
def send_command(self, cmd):
|
||||
if not self.sock:
|
||||
if not self.connect():
|
||||
return "ERROR: not connected"
|
||||
try:
|
||||
self.sock.sendall((cmd + "\n").encode())
|
||||
time.sleep(0.1)
|
||||
return self._recv()
|
||||
except (socket.error, BrokenPipeError):
|
||||
log.warning("gNB ctrl connection lost, reconnecting...")
|
||||
self.sock = None
|
||||
if not self.connect():
|
||||
return "ERROR: reconnect failed"
|
||||
self.sock.sendall((cmd + "\n").encode())
|
||||
time.sleep(0.1)
|
||||
return self._recv()
|
||||
|
||||
def _recv(self):
|
||||
self.sock.settimeout(0.5)
|
||||
data = b""
|
||||
try:
|
||||
while True:
|
||||
chunk = self.sock.recv(4096)
|
||||
if not chunk:
|
||||
break
|
||||
data += chunk
|
||||
except socket.timeout:
|
||||
pass
|
||||
return data.decode("utf-8", errors="ignore")
|
||||
|
||||
def close(self):
|
||||
if self.sock:
|
||||
try:
|
||||
self.sock.sendall(b"QUIT\n")
|
||||
except Exception:
|
||||
pass
|
||||
self.sock.close()
|
||||
self.sock = None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# NETCONF Session Handler
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class NetconfSession:
|
||||
"""Handle a single NETCONF session on an SSH channel."""
|
||||
|
||||
def __init__(self, channel, capability, gnb_ctrl):
|
||||
self.channel = channel
|
||||
self.cap = capability
|
||||
self.gnb = gnb_ctrl
|
||||
self.session_id = 1
|
||||
self.running = True
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self._send_hello()
|
||||
hello_msg = self._recv_message()
|
||||
if hello_msg is None:
|
||||
log.warning("Client did not send hello")
|
||||
return
|
||||
log.info("NETCONF hello exchange complete")
|
||||
|
||||
while self.running:
|
||||
msg = self._recv_message()
|
||||
if msg is None:
|
||||
break
|
||||
self._process_rpc(msg)
|
||||
except Exception as e:
|
||||
log.exception("Session error: %s", e)
|
||||
finally:
|
||||
try:
|
||||
self.channel.close()
|
||||
except Exception:
|
||||
pass
|
||||
log.info("NETCONF session closed")
|
||||
|
||||
def _send_hello(self):
|
||||
hello = (
|
||||
'<?xml version="1.0" encoding="UTF-8"?>'
|
||||
f'<hello xmlns="{NS_NC}">'
|
||||
"<capabilities>"
|
||||
"<capability>urn:ietf:params:netconf:base:1.0</capability>"
|
||||
"<capability>urn:ietf:params:netconf:capability:writable-running:1.0</capability>"
|
||||
"</capabilities>"
|
||||
f"<session-id>{self.session_id}</session-id>"
|
||||
"</hello>"
|
||||
)
|
||||
self._send_message(hello)
|
||||
|
||||
def _send_message(self, xml_str):
|
||||
data = (xml_str + NETCONF_END).encode("utf-8")
|
||||
self.channel.sendall(data)
|
||||
|
||||
def _recv_message(self):
|
||||
buf = b""
|
||||
while self.running:
|
||||
try:
|
||||
self.channel.settimeout(300)
|
||||
chunk = self.channel.recv(65536)
|
||||
if not chunk:
|
||||
return None
|
||||
buf += chunk
|
||||
text = buf.decode("utf-8", errors="ignore")
|
||||
if NETCONF_END in text:
|
||||
return text.split(NETCONF_END)[0].strip()
|
||||
except socket.timeout:
|
||||
continue
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
def _process_rpc(self, xml_str):
|
||||
# Strip XML declaration if present
|
||||
xml_str = re.sub(r"<\?xml[^?]*\?>", "", xml_str).strip()
|
||||
try:
|
||||
root = ET.fromstring(xml_str)
|
||||
except ET.ParseError as e:
|
||||
log.warning("Malformed RPC: %s", e)
|
||||
self._send_error("0", "malformed-message", "rpc")
|
||||
return
|
||||
|
||||
msg_id = root.get("message-id", "0")
|
||||
|
||||
# Find the operation element
|
||||
for child in root:
|
||||
local_tag = child.tag.split("}")[-1] if "}" in child.tag else child.tag
|
||||
|
||||
if local_tag == "edit-config":
|
||||
self._handle_edit_config(msg_id, child)
|
||||
elif local_tag == "get":
|
||||
self._handle_get(msg_id, child)
|
||||
elif local_tag == "get-config":
|
||||
self._handle_get_config(msg_id, child)
|
||||
elif local_tag == "close-session":
|
||||
self._send_ok(msg_id)
|
||||
self.running = False
|
||||
elif local_tag == "lock" or local_tag == "unlock":
|
||||
self._send_ok(msg_id)
|
||||
else:
|
||||
self._send_error(msg_id, "operation-not-supported", "protocol")
|
||||
return # Only process first operation
|
||||
|
||||
def _handle_edit_config(self, msg_id, edit_elem):
|
||||
"""Handle edit-config for tx-array-antenna-mask-config."""
|
||||
# Find <config> element
|
||||
config_elem = None
|
||||
for child in edit_elem:
|
||||
local = child.tag.split("}")[-1] if "}" in child.tag else child.tag
|
||||
if local == "config":
|
||||
config_elem = child
|
||||
break
|
||||
|
||||
if config_elem is None:
|
||||
self._send_error(msg_id, "missing-element", "protocol",
|
||||
"Missing <config> element")
|
||||
return
|
||||
|
||||
# Find tx-array-antenna-mask-config under user-plane-configuration
|
||||
mask_config = None
|
||||
for elem in config_elem.iter():
|
||||
local = elem.tag.split("}")[-1] if "}" in elem.tag else elem.tag
|
||||
if local == "tx-array-antenna-mask-config":
|
||||
mask_config = elem
|
||||
break
|
||||
|
||||
if mask_config is None:
|
||||
# No antenna mask config in this edit - accept silently
|
||||
self._send_ok(msg_id)
|
||||
return
|
||||
|
||||
# Extract array-name, antenna-bitmask-index or antenna-bitmask
|
||||
array_name = None
|
||||
bitmask_index = None
|
||||
bitmask_b64 = None
|
||||
|
||||
for child in mask_config:
|
||||
local = child.tag.split("}")[-1] if "}" in child.tag else child.tag
|
||||
if local == "array-name":
|
||||
array_name = child.text.strip() if child.text else None
|
||||
elif local == "antenna-bitmask-index":
|
||||
bitmask_index = int(child.text.strip()) if child.text else None
|
||||
elif local == "antenna-bitmask":
|
||||
bitmask_b64 = child.text.strip() if child.text else None
|
||||
|
||||
if not array_name:
|
||||
self._send_error(msg_id, "missing-element", "application",
|
||||
"Missing array-name in tx-array-antenna-mask-config")
|
||||
return
|
||||
|
||||
# Resolve the mask
|
||||
resolved = None
|
||||
if bitmask_index is not None:
|
||||
resolved, err = self.cap.resolve_index(array_name, bitmask_index)
|
||||
if not resolved:
|
||||
self._send_error(msg_id, "invalid-value", "application", err)
|
||||
return
|
||||
log.info("M-Plane edit-config: array=%s, antenna-bitmask-index=%d",
|
||||
array_name, bitmask_index)
|
||||
elif bitmask_b64 is not None:
|
||||
mask_int = b64_to_mask(bitmask_b64)
|
||||
resolved, err = self.cap.resolve_mask(array_name, mask_int)
|
||||
if not resolved:
|
||||
self._send_error(msg_id, "invalid-value", "application", err)
|
||||
return
|
||||
log.info("M-Plane edit-config: array=%s, antenna-bitmask=%s (0x%02X)",
|
||||
array_name, bitmask_b64, mask_int)
|
||||
else:
|
||||
self._send_error(msg_id, "missing-element", "application",
|
||||
"Need antenna-bitmask or antenna-bitmask-index")
|
||||
return
|
||||
|
||||
# Apply to gNB via internal control socket
|
||||
mask_int = resolved["int"]
|
||||
resp = self.gnb.send_command(f"SET TX MASK 0x{mask_int:02X}")
|
||||
log.info("Applied mask 0x%02X to gNB: %s", mask_int, resp.strip())
|
||||
|
||||
# Update current config
|
||||
self.cap.current_config[array_name] = {
|
||||
"index": resolved["index"],
|
||||
"mask_int": mask_int,
|
||||
}
|
||||
|
||||
# Log WG4 mplane-trx-control-ant-mask-update notification
|
||||
log.info("WG4 mplane-trx-control-ant-mask-update: "
|
||||
"array-name='%s', array-type=TX-ARRAY, "
|
||||
"updated-antenna-mask-index=%d, updated-antenna-mask=%s",
|
||||
array_name, resolved["index"], resolved["b64"])
|
||||
|
||||
self._send_ok(msg_id)
|
||||
|
||||
def _handle_get(self, msg_id, get_elem):
|
||||
"""Handle <get> - return operational data (capability + current config)."""
|
||||
data_xml = self.cap.get_capability_xml()
|
||||
reply = (
|
||||
f'<rpc-reply xmlns="{NS_NC}" message-id="{msg_id}">'
|
||||
f"<data>{data_xml}</data>"
|
||||
"</rpc-reply>"
|
||||
)
|
||||
self._send_message(reply)
|
||||
|
||||
def _handle_get_config(self, msg_id, get_elem):
|
||||
"""Handle <get-config> - return running config (current mask config)."""
|
||||
parts = [f'<user-plane-configuration xmlns="{NS_UPLANE}">']
|
||||
for aname, cfg in self.cap.current_config.items():
|
||||
parts.append("<tx-array-antenna-mask-config>")
|
||||
parts.append(f"<array-name>{aname}</array-name>")
|
||||
parts.append(f"<antenna-bitmask-index>{cfg['index']}</antenna-bitmask-index>")
|
||||
parts.append("</tx-array-antenna-mask-config>")
|
||||
parts.append("</user-plane-configuration>")
|
||||
reply = (
|
||||
f'<rpc-reply xmlns="{NS_NC}" message-id="{msg_id}">'
|
||||
f'<data>{"".join(parts)}</data>'
|
||||
"</rpc-reply>"
|
||||
)
|
||||
self._send_message(reply)
|
||||
|
||||
def _send_ok(self, msg_id):
|
||||
reply = (
|
||||
f'<rpc-reply xmlns="{NS_NC}" message-id="{msg_id}">'
|
||||
"<ok/>"
|
||||
"</rpc-reply>"
|
||||
)
|
||||
self._send_message(reply)
|
||||
|
||||
def _send_error(self, msg_id, error_tag, error_type, error_message=""):
|
||||
msg_part = ""
|
||||
if error_message:
|
||||
msg_part = f"<error-message>{error_message}</error-message>"
|
||||
reply = (
|
||||
f'<rpc-reply xmlns="{NS_NC}" message-id="{msg_id}">'
|
||||
"<rpc-error>"
|
||||
f"<error-type>{error_type}</error-type>"
|
||||
f"<error-tag>{error_tag}</error-tag>"
|
||||
"<error-severity>error</error-severity>"
|
||||
f"{msg_part}"
|
||||
"</rpc-error>"
|
||||
"</rpc-reply>"
|
||||
)
|
||||
self._send_message(reply)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SSH Server Interface
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class MplaneSSHServer(paramiko.ServerInterface):
|
||||
def __init__(self):
|
||||
self.subsystem_event = threading.Event()
|
||||
|
||||
def check_channel_request(self, kind, chanid):
|
||||
if kind == "session":
|
||||
return paramiko.OPEN_SUCCEEDED
|
||||
return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
|
||||
|
||||
def get_allowed_auths(self, username):
|
||||
return "password"
|
||||
|
||||
def check_auth_password(self, username, password):
|
||||
if username == MPLANE_USER and password == MPLANE_PASS:
|
||||
return paramiko.AUTH_SUCCESSFUL
|
||||
return paramiko.AUTH_FAILED
|
||||
|
||||
def check_channel_subsystem_request(self, channel, name):
|
||||
if name == "netconf":
|
||||
self.subsystem_event.set()
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main Server
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def handle_connection(client_sock, addr, host_key, capability, gnb_ctrl):
|
||||
"""Handle a single SSH/NETCONF client connection."""
|
||||
log.info("SSH connection from %s:%d", addr[0], addr[1])
|
||||
transport = None
|
||||
try:
|
||||
transport = paramiko.Transport(client_sock)
|
||||
transport.add_server_key(host_key)
|
||||
server = MplaneSSHServer()
|
||||
transport.start_server(server=server)
|
||||
|
||||
channel = transport.accept(30)
|
||||
if channel is None:
|
||||
log.warning("No channel opened by client")
|
||||
return
|
||||
|
||||
# Wait for NETCONF subsystem request
|
||||
if not server.subsystem_event.wait(10):
|
||||
log.warning("Client did not request netconf subsystem")
|
||||
channel.close()
|
||||
return
|
||||
|
||||
session = NetconfSession(channel, capability, gnb_ctrl)
|
||||
session.run()
|
||||
|
||||
except paramiko.SSHException as e:
|
||||
log.warning("SSH error from %s: %s", addr[0], e)
|
||||
except Exception as e:
|
||||
log.exception("Connection error from %s: %s", addr[0], e)
|
||||
finally:
|
||||
if transport:
|
||||
try:
|
||||
transport.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
log.info("O-RU M-Plane NETCONF server starting")
|
||||
log.info(" Port: %d", MPLANE_PORT)
|
||||
log.info(" gNB control: %s:%d (internal)", GNB_CTRL_HOST, GNB_CTRL_PORT)
|
||||
|
||||
# Load/generate SSH host key
|
||||
if os.path.exists(SSH_HOST_KEY):
|
||||
host_key = paramiko.RSAKey.from_private_key_file(SSH_HOST_KEY)
|
||||
log.info("Loaded SSH host key from %s", SSH_HOST_KEY)
|
||||
else:
|
||||
host_key = paramiko.RSAKey.generate(2048)
|
||||
os.makedirs(os.path.dirname(SSH_HOST_KEY), exist_ok=True)
|
||||
host_key.write_private_key_file(SSH_HOST_KEY)
|
||||
log.info("Generated SSH host key at %s", SSH_HOST_KEY)
|
||||
|
||||
# Load WG4 capability
|
||||
capability = Capability()
|
||||
capability.load(WG4_CAPABILITY_FILE)
|
||||
for name, arr in capability.tx_arrays.items():
|
||||
log.info(" tx-array '%s': %d supported masks", name, len(arr["masks"]))
|
||||
for m in arr["masks"]:
|
||||
log.info(" index=%d: %s (0x%02X, %d active)",
|
||||
m["index"], m["b64"], m["int"], bin(m["int"]).count("1"))
|
||||
|
||||
# Connect to gNB internal control
|
||||
gnb_ctrl = GnbCtrlClient(GNB_CTRL_HOST, GNB_CTRL_PORT)
|
||||
gnb_ctrl.connect()
|
||||
|
||||
# Create server socket
|
||||
server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
server_sock.bind(("0.0.0.0", MPLANE_PORT))
|
||||
server_sock.listen(5)
|
||||
server_sock.settimeout(1.0)
|
||||
|
||||
shutdown = threading.Event()
|
||||
|
||||
def handle_signal(signum, frame):
|
||||
log.info("Shutdown signal received")
|
||||
shutdown.set()
|
||||
|
||||
signal.signal(signal.SIGTERM, handle_signal)
|
||||
signal.signal(signal.SIGINT, handle_signal)
|
||||
|
||||
log.info("M-Plane NETCONF server ready on port %d", MPLANE_PORT)
|
||||
|
||||
while not shutdown.is_set():
|
||||
try:
|
||||
client_sock, addr = server_sock.accept()
|
||||
t = threading.Thread(
|
||||
target=handle_connection,
|
||||
args=(client_sock, addr, host_key, capability, gnb_ctrl),
|
||||
daemon=True,
|
||||
)
|
||||
t.start()
|
||||
except socket.timeout:
|
||||
continue
|
||||
except Exception as e:
|
||||
if not shutdown.is_set():
|
||||
log.exception("Accept error: %s", e)
|
||||
|
||||
log.info("Shutting down...")
|
||||
server_sock.close()
|
||||
gnb_ctrl.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"tx-arrays": [
|
||||
{
|
||||
"name": "tx-array-0",
|
||||
"number-of-columns": 4,
|
||||
"number-of-array-layers": 4,
|
||||
"mplane-trx-control-txarr-capability-info": {
|
||||
"mplane-supported-trx-control-masks": [
|
||||
{"index": 0, "antenna-mask": "Dw=="},
|
||||
{"index": 1, "antenna-mask": "Bw=="},
|
||||
{"index": 2, "antenna-mask": "Aw=="},
|
||||
{"index": 3, "antenna-mask": "AQ=="}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -158,6 +158,9 @@ void phy_init_nr_gNB(PHY_VARS_gNB *gNB)
|
||||
|
||||
init_DLSCH_struct(gNB);
|
||||
|
||||
nr_init_antenna_state(&gNB->antenna_state, Ptx, Prx, "tx-array-0", "rx-array-0");
|
||||
LOG_I(PHY, "[ANTENNA_STATE] Initialized with %d TX, %d RX antennas\n", Ptx, Prx);
|
||||
|
||||
gNB->nr_srs_info = (nr_srs_info_t **)malloc16_clear(gNB->max_nb_srs * sizeof(nr_srs_info_t*));
|
||||
for (int id = 0; id < gNB->max_nb_srs; id++) {
|
||||
gNB->nr_srs_info[id] = (nr_srs_info_t *)malloc16_clear(sizeof(nr_srs_info_t));
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
#include "modulation_eNB.h"
|
||||
#include "nr_modulation.h"
|
||||
#include "common/utils/LOG/vcd_signal_dumper.h"
|
||||
#include "PHY/NR_TRANSPORT/nr_antenna_state.h"
|
||||
|
||||
int beam_precoding(int32_t **txdataF,
|
||||
int32_t **txdataF_BF,
|
||||
@@ -143,3 +144,35 @@ void nr_beam_precoding(c16_t **txdataF,
|
||||
15);
|
||||
}
|
||||
}
|
||||
|
||||
void nr_beam_precoding_with_muting(c16_t **txdataF,
|
||||
c16_t **txdataF_BF,
|
||||
NR_DL_FRAME_PARMS *frame_parms,
|
||||
int32_t ***beam_weights,
|
||||
int slot,
|
||||
int symbol,
|
||||
int aa,
|
||||
int nb_antenna_ports,
|
||||
int offset,
|
||||
const nr_antenna_state_t *ant_state)
|
||||
{
|
||||
// Clear output buffer first
|
||||
memset(&txdataF_BF[aa][symbol * frame_parms->ofdm_symbol_size], 0,
|
||||
sizeof(c16_t) * frame_parms->ofdm_symbol_size);
|
||||
// O-RAN energy saving: skip precoding if antenna is muted
|
||||
// This implements transmission blanking per WG4 spec
|
||||
if (ant_state != NULL && !nr_is_tx_antenna_active(ant_state, aa)) {
|
||||
// Antenna is muted - output remains zero (transmission blank)
|
||||
LOG_D(PHY, "[ANTENNA_MUTE] Antenna %d muted, blanking transmission for symbol %d\n",
|
||||
aa, symbol);
|
||||
return;
|
||||
}
|
||||
// Normal precoding for active antennas
|
||||
for (int p = 0; p < nb_antenna_ports; p++) {
|
||||
multadd_cpx_vector((c16_t *)&txdataF[p][(symbol * frame_parms->ofdm_symbol_size) + offset],
|
||||
(c16_t *)beam_weights[p][aa],
|
||||
(c16_t *)&txdataF_BF[aa][symbol * frame_parms->ofdm_symbol_size],
|
||||
frame_parms->ofdm_symbol_size,
|
||||
15);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +103,35 @@ void nr_beam_precoding(c16_t **txdataF,
|
||||
int nb_antenna_ports,
|
||||
int offset);
|
||||
|
||||
|
||||
/*! \brief NR beam precoding with O-RAN compliant antenna muting
|
||||
*
|
||||
* Extends nr_beam_precoding to support antenna muting for energy saving
|
||||
* per O-RAN WG4 MPLANE-TRX-CONTROL specification (o-ran-uplane-conf.yang).
|
||||
* When an antenna is muted, transmission is blanked (output zeroed).
|
||||
*
|
||||
* @param[in] txdataF Input frequency-domain data per antenna port
|
||||
* @param[out] txdataF_BF Output beamformed data per physical antenna
|
||||
* @param[in] frame_parms Frame parameters
|
||||
* @param[in] beam_weights Beamforming weights
|
||||
* @param[in] slot Current slot number
|
||||
* @param[in] symbol Current symbol number
|
||||
* @param[in] aa Physical antenna index
|
||||
* @param[in] nb_antenna_ports Number of antenna ports
|
||||
* @param[in] offset Frequency offset
|
||||
* @param[in] ant_state Antenna state for muting control (NULL = all active)
|
||||
*/
|
||||
void nr_beam_precoding_with_muting(c16_t **txdataF,
|
||||
c16_t **txdataF_BF,
|
||||
NR_DL_FRAME_PARMS *frame_parms,
|
||||
int32_t ***beam_weights,
|
||||
int slot,
|
||||
int symbol,
|
||||
int aa,
|
||||
int nb_antenna_ports,
|
||||
int offset,
|
||||
const nr_antenna_state_t *ant_state);
|
||||
|
||||
void apply_nr_rotation_TX(const NR_DL_FRAME_PARMS *fp,
|
||||
c16_t *txdataF,
|
||||
const c16_t *symbol_rotation,
|
||||
|
||||
@@ -0,0 +1,532 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
/*! \file PHY/NR_TRANSPORT/nr_antenna_control.c
|
||||
* \brief Antenna control interface implementation with O-RAN M-Plane support
|
||||
* \author OpenAirInterface
|
||||
* \date 2025
|
||||
* \version 2.0
|
||||
*/
|
||||
|
||||
#include "nr_antenna_control.h"
|
||||
#include "common/utils/LOG/log.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <pthread.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <errno.h>
|
||||
#include <signal.h>
|
||||
|
||||
// Try to include M-Plane header if available
|
||||
#ifdef OAI_MPLANE
|
||||
#include "radio/fhi_72/mplane/antenna-control.h"
|
||||
#include "radio/fhi_72/mplane/ru-mplane-api.h"
|
||||
#endif
|
||||
|
||||
// Global control state
|
||||
static struct {
|
||||
pthread_t thread;
|
||||
int server_fd;
|
||||
nr_antenna_state_t *ant_state;
|
||||
volatile bool running;
|
||||
int port;
|
||||
nr_antenna_ctrl_backend_t backend;
|
||||
#ifdef OAI_MPLANE
|
||||
ru_session_t *ru_session;
|
||||
#else
|
||||
void *ru_session;
|
||||
#endif
|
||||
} g_ctrl = {0};
|
||||
|
||||
// ============================================================================
|
||||
// Test Socket Server Implementation (Development/Testing Mode)
|
||||
// ============================================================================
|
||||
|
||||
// Process a single command and generate response
|
||||
static void process_command(const char *cmd, char *response, size_t resp_size)
|
||||
{
|
||||
char cmd_upper[256];
|
||||
strncpy(cmd_upper, cmd, sizeof(cmd_upper) - 1);
|
||||
cmd_upper[sizeof(cmd_upper) - 1] = '\0';
|
||||
|
||||
// Convert to uppercase for case-insensitive matching
|
||||
for (char *p = cmd_upper; *p; p++) {
|
||||
if (*p >= 'a' && *p <= 'z') *p -= 32;
|
||||
}
|
||||
|
||||
// Trim whitespace
|
||||
char *p = cmd_upper;
|
||||
while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') p++;
|
||||
char *end = p + strlen(p) - 1;
|
||||
while (end > p && (*end == ' ' || *end == '\t' || *end == '\r' || *end == '\n')) *end-- = '\0';
|
||||
|
||||
if (strncmp(p, "MUTE TX ", 8) == 0) {
|
||||
int ant_id = atoi(p + 8);
|
||||
nr_mute_tx_antenna(g_ctrl.ant_state, ant_id);
|
||||
snprintf(response, resp_size, "OK: TX antenna %d muted\n", ant_id);
|
||||
}
|
||||
else if (strncmp(p, "UNMUTE TX ", 10) == 0) {
|
||||
int ant_id = atoi(p + 10);
|
||||
nr_unmute_tx_antenna(g_ctrl.ant_state, ant_id);
|
||||
snprintf(response, resp_size, "OK: TX antenna %d unmuted\n", ant_id);
|
||||
}
|
||||
else if (strncmp(p, "MUTE RX ", 8) == 0) {
|
||||
int ant_id = atoi(p + 8);
|
||||
nr_mute_rx_antenna(g_ctrl.ant_state, ant_id);
|
||||
snprintf(response, resp_size, "OK: RX antenna %d muted\n", ant_id);
|
||||
}
|
||||
else if (strncmp(p, "UNMUTE RX ", 10) == 0) {
|
||||
int ant_id = atoi(p + 10);
|
||||
nr_unmute_rx_antenna(g_ctrl.ant_state, ant_id);
|
||||
snprintf(response, resp_size, "OK: RX antenna %d unmuted\n", ant_id);
|
||||
}
|
||||
else if (strncmp(p, "SET TX MASK ", 12) == 0) {
|
||||
unsigned int mask;
|
||||
if (sscanf(p + 12, "%x", &mask) == 1 || sscanf(p + 12, "0X%x", &mask) == 1) {
|
||||
uint8_t old = nr_set_tx_antenna_bitmask(g_ctrl.ant_state, (uint8_t)mask);
|
||||
snprintf(response, resp_size, "OK: TX mask 0x%02X -> 0x%02X\n", old, (uint8_t)mask);
|
||||
} else {
|
||||
snprintf(response, resp_size, "ERROR: Invalid mask format. Use hex (e.g., 0F or 0x0F)\n");
|
||||
}
|
||||
}
|
||||
else if (strncmp(p, "SET RX MASK ", 12) == 0) {
|
||||
unsigned int mask;
|
||||
if (sscanf(p + 12, "%x", &mask) == 1 || sscanf(p + 12, "0X%x", &mask) == 1) {
|
||||
uint8_t old = nr_set_rx_antenna_bitmask(g_ctrl.ant_state, (uint8_t)mask);
|
||||
snprintf(response, resp_size, "OK: RX mask 0x%02X -> 0x%02X\n", old, (uint8_t)mask);
|
||||
} else {
|
||||
snprintf(response, resp_size, "ERROR: Invalid mask format. Use hex (e.g., 0F or 0x0F)\n");
|
||||
}
|
||||
}
|
||||
else if (strcmp(p, "STATUS") == 0) {
|
||||
bool in_recovery = nr_is_in_recovery_mode(g_ctrl.ant_state);
|
||||
snprintf(response, resp_size,
|
||||
"=== Antenna Status ===\n"
|
||||
"TX Array: %s\n"
|
||||
" Antennas: %d configured, %d active\n"
|
||||
" Bitmask: 0x%02X\n"
|
||||
" Antenna 0 protected: %s\n"
|
||||
"RX Array: %s\n"
|
||||
" Antennas: %d configured, %d active\n"
|
||||
" Bitmask: 0x%02X\n"
|
||||
"Recovery Mode: %s\n"
|
||||
"Backend: %s\n",
|
||||
g_ctrl.ant_state->tx_array_name,
|
||||
g_ctrl.ant_state->num_tx_antennas,
|
||||
nr_get_active_tx_antenna_count(g_ctrl.ant_state),
|
||||
nr_get_tx_antenna_bitmask(g_ctrl.ant_state),
|
||||
g_ctrl.ant_state->config.protect_antenna0 ? "YES" : "NO",
|
||||
g_ctrl.ant_state->rx_array_name,
|
||||
g_ctrl.ant_state->num_rx_antennas,
|
||||
nr_get_active_rx_antenna_count(g_ctrl.ant_state),
|
||||
nr_get_rx_antenna_bitmask(g_ctrl.ant_state),
|
||||
in_recovery ? "ACTIVE (use identity matrix)" : "OFF",
|
||||
g_ctrl.backend == ANTENNA_CTRL_BACKEND_MPLANE ? "O-RAN M-Plane NETCONF" :
|
||||
g_ctrl.backend == ANTENNA_CTRL_BACKEND_TEST_SOCKET ? "Test Socket (NOT O-RAN)" :
|
||||
"None");
|
||||
}
|
||||
else if (strcmp(p, "STATS") == 0) {
|
||||
uint64_t tx_events, rx_events, muted_slots;
|
||||
nr_get_antenna_stats(g_ctrl.ant_state, &tx_events, &rx_events, &muted_slots);
|
||||
snprintf(response, resp_size,
|
||||
"=== Energy Saving Statistics ===\n"
|
||||
"TX mute events: %lu\n"
|
||||
"RX mute events: %lu\n"
|
||||
"Total muted slots: %lu\n",
|
||||
tx_events, rx_events, muted_slots);
|
||||
}
|
||||
else if (strcmp(p, "RESET STATS") == 0) {
|
||||
nr_reset_antenna_stats(g_ctrl.ant_state);
|
||||
snprintf(response, resp_size, "OK: Statistics reset\n");
|
||||
}
|
||||
else if (strcmp(p, "RESET") == 0 || strcmp(p, "UNMUTE ALL") == 0) {
|
||||
// Reset all antennas to active state with recovery mode
|
||||
nr_unmute_all_tx_antennas(g_ctrl.ant_state);
|
||||
nr_unmute_all_rx_antennas(g_ctrl.ant_state);
|
||||
snprintf(response, resp_size,
|
||||
"OK: All antennas unmuted (recovery mode: %u ms)\n"
|
||||
" TX mask: 0x%02X (%d antennas)\n"
|
||||
" RX mask: 0x%02X (%d antennas)\n",
|
||||
g_ctrl.ant_state->recovery.recovery_duration_ms,
|
||||
nr_get_tx_antenna_bitmask(g_ctrl.ant_state), g_ctrl.ant_state->num_tx_antennas,
|
||||
nr_get_rx_antenna_bitmask(g_ctrl.ant_state), g_ctrl.ant_state->num_rx_antennas);
|
||||
}
|
||||
else if (strcmp(p, "PROTECT ANT0 ON") == 0) {
|
||||
nr_set_antenna0_protection(g_ctrl.ant_state, true);
|
||||
snprintf(response, resp_size, "OK: Antenna 0 protection enabled (SSB)\n");
|
||||
}
|
||||
else if (strcmp(p, "PROTECT ANT0 OFF") == 0) {
|
||||
nr_set_antenna0_protection(g_ctrl.ant_state, false);
|
||||
snprintf(response, resp_size, "OK: Antenna 0 protection disabled (WARNING: may cause UE sync loss)\n");
|
||||
}
|
||||
else if (strcmp(p, "SYNC") == 0) {
|
||||
// Synchronize to RU (only works with M-Plane backend)
|
||||
if (nr_antenna_control_sync_to_ru(g_ctrl.ant_state)) {
|
||||
snprintf(response, resp_size, "OK: Synchronized to RU\n");
|
||||
} else {
|
||||
snprintf(response, resp_size, "ERROR: Sync failed (M-Plane backend required)\n");
|
||||
}
|
||||
}
|
||||
else if (strcmp(p, "HELP") == 0) {
|
||||
snprintf(response, resp_size,
|
||||
"=== Antenna Control ===\n"
|
||||
"MUTE TX/RX <id> - Mute antenna\n"
|
||||
"UNMUTE TX/RX <id> - Unmute antenna\n"
|
||||
"SET TX/RX MASK <hex> - Set bitmask\n"
|
||||
"STATUS - Show state\n"
|
||||
"STATS - Show statistics\n"
|
||||
"RESET - Unmute all\n"
|
||||
"PROTECT ANT0 ON/OFF\n"
|
||||
"HELP/QUIT\n");
|
||||
}
|
||||
else if (strcmp(p, "QUIT") == 0 || strcmp(p, "EXIT") == 0) {
|
||||
snprintf(response, resp_size, "BYE\n");
|
||||
}
|
||||
else if (strlen(p) == 0) {
|
||||
response[0] = '\0'; // Empty command, no response
|
||||
}
|
||||
else {
|
||||
snprintf(response, resp_size, "ERROR: Unknown command '%s'. Type HELP for usage.\n", cmd);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle a single client connection
|
||||
static void handle_client(int client_fd)
|
||||
{
|
||||
char buffer[256];
|
||||
char response[1024];
|
||||
|
||||
// Send welcome message with warning about non-compliance
|
||||
const char *welcome =
|
||||
"=== OAI Antenna Control Test Server ===\n"
|
||||
"WARNING: This is a TEST interface, NOT O-RAN compliant!\n"
|
||||
"For production, use NETCONF via radio/fhi_72/mplane/\n"
|
||||
"Type HELP for commands.\n\n";
|
||||
send(client_fd, welcome, strlen(welcome), 0);
|
||||
|
||||
while (g_ctrl.running) {
|
||||
// Send prompt
|
||||
send(client_fd, "> ", 2, 0);
|
||||
|
||||
// Receive command
|
||||
ssize_t n = recv(client_fd, buffer, sizeof(buffer) - 1, 0);
|
||||
if (n <= 0) break;
|
||||
|
||||
buffer[n] = '\0';
|
||||
|
||||
// Process command
|
||||
process_command(buffer, response, sizeof(response));
|
||||
|
||||
// Send response
|
||||
if (strlen(response) > 0) {
|
||||
send(client_fd, response, strlen(response), 0);
|
||||
}
|
||||
|
||||
// Check for quit
|
||||
if (strstr(buffer, "QUIT") || strstr(buffer, "quit") ||
|
||||
strstr(buffer, "EXIT") || strstr(buffer, "exit")) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
close(client_fd);
|
||||
LOG_I(PHY, "[ANTENNA_CTRL] Client disconnected\n");
|
||||
}
|
||||
|
||||
// Server thread function
|
||||
static void *server_thread(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
|
||||
struct sockaddr_in client_addr;
|
||||
socklen_t addr_len = sizeof(client_addr);
|
||||
|
||||
LOG_I(PHY, "[ANTENNA_CTRL] Test server listening on port %d\n", g_ctrl.port);
|
||||
|
||||
while (g_ctrl.running) {
|
||||
// Accept connection with timeout
|
||||
fd_set read_fds;
|
||||
FD_ZERO(&read_fds);
|
||||
FD_SET(g_ctrl.server_fd, &read_fds);
|
||||
|
||||
struct timeval tv = {.tv_sec = 1, .tv_usec = 0};
|
||||
int ret = select(g_ctrl.server_fd + 1, &read_fds, NULL, NULL, &tv);
|
||||
|
||||
if (ret <= 0) continue;
|
||||
|
||||
int client_fd = accept(g_ctrl.server_fd, (struct sockaddr *)&client_addr, &addr_len);
|
||||
if (client_fd < 0) continue;
|
||||
|
||||
LOG_I(PHY, "[ANTENNA_CTRL] Client connected from %s:%d\n",
|
||||
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
|
||||
|
||||
handle_client(client_fd);
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Public API Implementation
|
||||
// ============================================================================
|
||||
|
||||
bool nr_antenna_control_init_mplane(nr_antenna_state_t *ant_state, void *ru_session)
|
||||
{
|
||||
#ifdef OAI_MPLANE
|
||||
if (g_ctrl.running) {
|
||||
LOG_W(PHY, "[ANTENNA_CTRL] Control interface already active\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ant_state == NULL || ru_session == NULL) {
|
||||
LOG_E(PHY, "[ANTENNA_CTRL] Invalid parameters\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
g_ctrl.ant_state = ant_state;
|
||||
g_ctrl.ru_session = (ru_session_t *)ru_session;
|
||||
g_ctrl.backend = ANTENNA_CTRL_BACKEND_MPLANE;
|
||||
g_ctrl.running = true;
|
||||
|
||||
// Check if RU supports TRX control feature
|
||||
if (!mplane_supports_trx_control(g_ctrl.ru_session)) {
|
||||
LOG_W(PHY, "[ANTENNA_CTRL] RU does not support MPLANE-TRX-CONTROL feature\n");
|
||||
LOG_W(PHY, "[ANTENNA_CTRL] Antenna muting may not work on this RU\n");
|
||||
}
|
||||
|
||||
LOG_I(PHY, "[ANTENNA_CTRL] Initialized with O-RAN M-Plane NETCONF backend\n");
|
||||
LOG_I(PHY, "[ANTENNA_CTRL] TX array: %s, RX array: %s\n",
|
||||
ant_state->tx_array_name, ant_state->rx_array_name);
|
||||
|
||||
return true;
|
||||
#else
|
||||
LOG_E(PHY, "[ANTENNA_CTRL] M-Plane support not compiled in (OAI_MPLANE not defined)\n");
|
||||
LOG_E(PHY, "[ANTENNA_CTRL] Use nr_antenna_control_start_test_server() for testing\n");
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool nr_antenna_control_start_test_server(nr_antenna_state_t *ant_state, int port)
|
||||
{
|
||||
if (g_ctrl.running) {
|
||||
LOG_W(PHY, "[ANTENNA_CTRL] Control interface already active\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ant_state == NULL) {
|
||||
LOG_E(PHY, "[ANTENNA_CTRL] Antenna state is NULL\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_W(PHY, "[ANTENNA_CTRL] ======================================================\n");
|
||||
LOG_W(PHY, "[ANTENNA_CTRL] Starting TEST socket server - NOT O-RAN COMPLIANT!\n");
|
||||
LOG_W(PHY, "[ANTENNA_CTRL] For production O-RAN compliance, use M-Plane NETCONF\n");
|
||||
LOG_W(PHY, "[ANTENNA_CTRL] See: radio/fhi_72/mplane/antenna-control-mplane.h\n");
|
||||
LOG_W(PHY, "[ANTENNA_CTRL] ======================================================\n");
|
||||
|
||||
g_ctrl.ant_state = ant_state;
|
||||
g_ctrl.port = (port > 0) ? port : NR_ANTENNA_CONTROL_TEST_PORT;
|
||||
g_ctrl.backend = ANTENNA_CTRL_BACKEND_TEST_SOCKET;
|
||||
|
||||
// Create socket
|
||||
g_ctrl.server_fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (g_ctrl.server_fd < 0) {
|
||||
LOG_E(PHY, "[ANTENNA_CTRL] Failed to create socket: %s\n", strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allow address reuse
|
||||
int opt = 1;
|
||||
setsockopt(g_ctrl.server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
|
||||
|
||||
// Bind
|
||||
struct sockaddr_in addr = {
|
||||
.sin_family = AF_INET,
|
||||
.sin_addr.s_addr = htonl(INADDR_ANY),
|
||||
.sin_port = htons(g_ctrl.port)
|
||||
};
|
||||
|
||||
if (bind(g_ctrl.server_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
|
||||
LOG_E(PHY, "[ANTENNA_CTRL] Failed to bind port %d: %s\n", g_ctrl.port, strerror(errno));
|
||||
close(g_ctrl.server_fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Listen
|
||||
if (listen(g_ctrl.server_fd, 1) < 0) {
|
||||
LOG_E(PHY, "[ANTENNA_CTRL] Failed to listen: %s\n", strerror(errno));
|
||||
close(g_ctrl.server_fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Start server thread
|
||||
g_ctrl.running = true;
|
||||
if (pthread_create(&g_ctrl.thread, NULL, server_thread, NULL) != 0) {
|
||||
LOG_E(PHY, "[ANTENNA_CTRL] Failed to create server thread\n");
|
||||
g_ctrl.running = false;
|
||||
close(g_ctrl.server_fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_I(PHY, "[ANTENNA_CTRL] Test server started on port %d\n", g_ctrl.port);
|
||||
LOG_I(PHY, "[ANTENNA_CTRL] Connect with: nc localhost %d\n", g_ctrl.port);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool nr_antenna_control_start(nr_antenna_state_t *ant_state, int port)
|
||||
{
|
||||
LOG_W(PHY, "[ANTENNA_CTRL] nr_antenna_control_start() is DEPRECATED\n");
|
||||
LOG_W(PHY, "[ANTENNA_CTRL] Use nr_antenna_control_init_mplane() for O-RAN compliance\n");
|
||||
LOG_W(PHY, "[ANTENNA_CTRL] or nr_antenna_control_start_test_server() for testing\n");
|
||||
|
||||
return nr_antenna_control_start_test_server(ant_state, port);
|
||||
}
|
||||
|
||||
void nr_antenna_control_stop(void)
|
||||
{
|
||||
if (!g_ctrl.running) return;
|
||||
|
||||
g_ctrl.running = false;
|
||||
|
||||
if (g_ctrl.backend == ANTENNA_CTRL_BACKEND_TEST_SOCKET) {
|
||||
// Close server socket to unblock accept()
|
||||
if (g_ctrl.server_fd >= 0) {
|
||||
close(g_ctrl.server_fd);
|
||||
g_ctrl.server_fd = -1;
|
||||
}
|
||||
|
||||
// Wait for thread
|
||||
pthread_join(g_ctrl.thread, NULL);
|
||||
}
|
||||
|
||||
g_ctrl.backend = ANTENNA_CTRL_BACKEND_NONE;
|
||||
g_ctrl.ant_state = NULL;
|
||||
g_ctrl.ru_session = NULL;
|
||||
|
||||
LOG_I(PHY, "[ANTENNA_CTRL] Control interface stopped\n");
|
||||
}
|
||||
|
||||
bool nr_antenna_control_is_running(void)
|
||||
{
|
||||
return g_ctrl.running;
|
||||
}
|
||||
|
||||
nr_antenna_ctrl_backend_t nr_antenna_control_get_backend(void)
|
||||
{
|
||||
return g_ctrl.backend;
|
||||
}
|
||||
|
||||
bool nr_antenna_control_sync_to_ru(nr_antenna_state_t *ant_state)
|
||||
{
|
||||
#ifdef OAI_MPLANE
|
||||
if (g_ctrl.backend != ANTENNA_CTRL_BACKEND_MPLANE || g_ctrl.ru_session == NULL) {
|
||||
LOG_W(PHY, "[ANTENNA_CTRL] M-Plane backend not active, cannot sync to RU\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ant_state == NULL) {
|
||||
ant_state = g_ctrl.ant_state;
|
||||
}
|
||||
|
||||
if (ant_state == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Send TX antenna mask to RU
|
||||
antenna_ctrl_result_t result;
|
||||
result = mplane_set_tx_antenna_mask(g_ctrl.ru_session,
|
||||
ant_state->tx_array_name,
|
||||
nr_get_tx_antenna_bitmask(ant_state),
|
||||
ant_state->num_tx_antennas);
|
||||
if (result != ANTENNA_CTRL_SUCCESS) {
|
||||
LOG_W(PHY, "[ANTENNA_CTRL] Failed to sync TX mask: %s\n",
|
||||
antenna_ctrl_result_str(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Send RX antenna mask to RU
|
||||
result = mplane_set_rx_antenna_mask(g_ctrl.ru_session,
|
||||
ant_state->rx_array_name,
|
||||
nr_get_rx_antenna_bitmask(ant_state),
|
||||
ant_state->num_rx_antennas);
|
||||
if (result != ANTENNA_CTRL_SUCCESS) {
|
||||
LOG_W(PHY, "[ANTENNA_CTRL] Failed to sync RX mask: %s\n",
|
||||
antenna_ctrl_result_str(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_I(PHY, "[ANTENNA_CTRL] Successfully synchronized antenna masks to RU\n");
|
||||
return true;
|
||||
#else
|
||||
LOG_W(PHY, "[ANTENNA_CTRL] M-Plane not compiled in\n");
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool nr_antenna_control_sync_from_ru(nr_antenna_state_t *ant_state)
|
||||
{
|
||||
#ifdef OAI_MPLANE
|
||||
if (g_ctrl.backend != ANTENNA_CTRL_BACKEND_MPLANE || g_ctrl.ru_session == NULL) {
|
||||
LOG_W(PHY, "[ANTENNA_CTRL] M-Plane backend not active, cannot sync from RU\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ant_state == NULL) {
|
||||
ant_state = g_ctrl.ant_state;
|
||||
}
|
||||
|
||||
if (ant_state == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t tx_mask, rx_mask;
|
||||
antenna_ctrl_result_t result;
|
||||
|
||||
// Get TX antenna mask from RU
|
||||
result = mplane_get_tx_antenna_mask(g_ctrl.ru_session,
|
||||
ant_state->tx_array_name,
|
||||
&tx_mask);
|
||||
if (result == ANTENNA_CTRL_SUCCESS) {
|
||||
nr_set_tx_antenna_bitmask(ant_state, tx_mask);
|
||||
}
|
||||
|
||||
// Get RX antenna mask from RU
|
||||
result = mplane_get_rx_antenna_mask(g_ctrl.ru_session,
|
||||
ant_state->rx_array_name,
|
||||
&rx_mask);
|
||||
if (result == ANTENNA_CTRL_SUCCESS) {
|
||||
nr_set_rx_antenna_bitmask(ant_state, rx_mask);
|
||||
}
|
||||
|
||||
LOG_I(PHY, "[ANTENNA_CTRL] Synchronized antenna masks from RU: TX=0x%02X, RX=0x%02X\n",
|
||||
tx_mask, rx_mask);
|
||||
return true;
|
||||
#else
|
||||
LOG_W(PHY, "[ANTENNA_CTRL] M-Plane not compiled in\n");
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
#ifndef NR_ANTENNA_CONTROL_H
|
||||
#define NR_ANTENNA_CONTROL_H
|
||||
|
||||
#include "nr_antenna_state.h"
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/// Default port for the test socket server (development mode only)
|
||||
#define NR_ANTENNA_CONTROL_TEST_PORT 9999
|
||||
|
||||
/**
|
||||
* @brief Antenna control backend type
|
||||
*/
|
||||
typedef enum {
|
||||
ANTENNA_CTRL_BACKEND_NONE = 0, ///< No external control
|
||||
ANTENNA_CTRL_BACKEND_MPLANE, ///< O-RAN M-Plane NETCONF (production)
|
||||
ANTENNA_CTRL_BACKEND_TEST_SOCKET ///< Test socket interface (development)
|
||||
} nr_antenna_ctrl_backend_t;
|
||||
|
||||
/**
|
||||
* @brief Initialize antenna control with O-RAN M-Plane NETCONF backend
|
||||
*
|
||||
* This is the O-RAN compliant production interface. It connects the
|
||||
* antenna state to a real NETCONF session to an O-RU.
|
||||
*
|
||||
* When antenna masks are changed via nr_set_tx_antenna_bitmask() or
|
||||
* similar functions, the changes are automatically propagated to the
|
||||
* RU via NETCONF edit-config operations.
|
||||
*
|
||||
* @param ant_state Pointer to the antenna state to control
|
||||
* @param ru_session Active M-Plane NETCONF session (from radio/fhi_72/mplane/)
|
||||
* @return true if initialization successful
|
||||
*/
|
||||
bool nr_antenna_control_init_mplane(nr_antenna_state_t *ant_state,
|
||||
void *ru_session);
|
||||
|
||||
/**
|
||||
* @brief Start the test socket server (development mode only)
|
||||
*
|
||||
* Creates a simple TCP server for testing antenna control without
|
||||
* NETCONF infrastructure. This is NOT O-RAN compliant.
|
||||
*
|
||||
* Commands supported (for backwards compatibility):
|
||||
* MUTE TX <ant_id> - Mute TX antenna
|
||||
* UNMUTE TX <ant_id> - Unmute TX antenna
|
||||
* MUTE RX <ant_id> - Mute RX antenna
|
||||
* UNMUTE RX <ant_id> - Unmute RX antenna
|
||||
* SET TX MASK <hex> - Set TX antenna bitmask
|
||||
* SET RX MASK <hex> - Set RX antenna bitmask
|
||||
* STATUS - Get current antenna state
|
||||
* STATS - Get energy saving statistics
|
||||
* RESET STATS - Reset statistics
|
||||
* QUIT - Close connection
|
||||
*
|
||||
* @param ant_state Pointer to the antenna state to control
|
||||
* @param port TCP port to listen on (use 0 for default)
|
||||
* @return true if server started successfully
|
||||
*
|
||||
* @note This function logs a warning that it's not O-RAN compliant
|
||||
*/
|
||||
bool nr_antenna_control_start_test_server(nr_antenna_state_t *ant_state, int port);
|
||||
|
||||
/**
|
||||
* @brief Legacy function - start antenna control server
|
||||
*
|
||||
* @deprecated Use nr_antenna_control_init_mplane() for O-RAN compliance
|
||||
* or nr_antenna_control_start_test_server() for testing.
|
||||
*
|
||||
* This function starts the test socket server for backwards compatibility.
|
||||
* It logs a deprecation warning.
|
||||
*/
|
||||
bool nr_antenna_control_start(nr_antenna_state_t *ant_state, int port);
|
||||
|
||||
/**
|
||||
* @brief Stop the antenna control interface
|
||||
*
|
||||
* Stops the test socket server if running.
|
||||
* For M-Plane backend, this disconnects from NETCONF notifications.
|
||||
*/
|
||||
void nr_antenna_control_stop(void);
|
||||
|
||||
/**
|
||||
* @brief Check if antenna control is active
|
||||
* @return true if control interface is running
|
||||
*/
|
||||
bool nr_antenna_control_is_running(void);
|
||||
|
||||
/**
|
||||
* @brief Get current antenna control backend type
|
||||
* @return Backend type currently in use
|
||||
*/
|
||||
nr_antenna_ctrl_backend_t nr_antenna_control_get_backend(void);
|
||||
|
||||
/**
|
||||
* @brief Synchronize local antenna state to RU via M-Plane NETCONF
|
||||
*
|
||||
* Sends the current antenna bitmask configuration to the RU.
|
||||
* Only applicable when using the M-Plane NETCONF backend.
|
||||
*
|
||||
* @param ant_state Pointer to the antenna state
|
||||
* @return true if synchronization successful or not applicable
|
||||
*/
|
||||
bool nr_antenna_control_sync_to_ru(nr_antenna_state_t *ant_state);
|
||||
|
||||
/**
|
||||
* @brief Synchronize RU antenna state to local via M-Plane NETCONF
|
||||
*
|
||||
* Reads the current antenna bitmask from the RU and updates local state.
|
||||
* Only applicable when using the M-Plane NETCONF backend.
|
||||
*
|
||||
* @param ant_state Pointer to the antenna state
|
||||
* @return true if synchronization successful or not applicable
|
||||
*/
|
||||
bool nr_antenna_control_sync_from_ru(nr_antenna_state_t *ant_state);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NR_ANTENNA_CONTROL_H */
|
||||
@@ -0,0 +1,367 @@
|
||||
#include "nr_antenna_state.h"
|
||||
#include "common/utils/LOG/log.h"
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
// Helper to count set bits in a byte
|
||||
static inline int popcount8(uint8_t x) {
|
||||
int count = 0;
|
||||
while (x) {
|
||||
count += x & 1;
|
||||
x >>= 1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// Get current timestamp in microseconds
|
||||
static inline uint64_t get_timestamp_us(void) {
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (uint64_t)ts.tv_sec * 1000000ULL + (uint64_t)ts.tv_nsec / 1000ULL;
|
||||
}
|
||||
|
||||
void nr_init_antenna_state(nr_antenna_state_t *state,
|
||||
uint8_t num_tx,
|
||||
uint8_t num_rx,
|
||||
const char *tx_array_name,
|
||||
const char *rx_array_name)
|
||||
{
|
||||
if (state == NULL) return;
|
||||
|
||||
memset(state, 0, sizeof(nr_antenna_state_t));
|
||||
|
||||
state->num_tx_antennas = (num_tx > NR_MAX_ANTENNAS) ? NR_MAX_ANTENNAS : num_tx;
|
||||
state->num_rx_antennas = (num_rx > NR_MAX_ANTENNAS) ? NR_MAX_ANTENNAS : num_rx;
|
||||
|
||||
// Initialize with all configured antennas active
|
||||
// Create bitmask with num_tx/num_rx lowest bits set
|
||||
state->tx_antenna_bitmask = (1 << state->num_tx_antennas) - 1;
|
||||
state->rx_antenna_bitmask = (1 << state->num_rx_antennas) - 1;
|
||||
|
||||
// Set O-RAN array names
|
||||
if (tx_array_name) {
|
||||
strncpy(state->tx_array_name, tx_array_name, sizeof(state->tx_array_name) - 1);
|
||||
} else {
|
||||
strncpy(state->tx_array_name, "tx-array-0", sizeof(state->tx_array_name) - 1);
|
||||
}
|
||||
|
||||
if (rx_array_name) {
|
||||
strncpy(state->rx_array_name, rx_array_name, sizeof(state->rx_array_name) - 1);
|
||||
} else {
|
||||
strncpy(state->rx_array_name, "rx-array-0", sizeof(state->rx_array_name) - 1);
|
||||
}
|
||||
|
||||
pthread_mutex_init(&state->lock, NULL);
|
||||
|
||||
state->stats.last_update_timestamp = get_timestamp_us();
|
||||
|
||||
// Initialize recovery mode
|
||||
state->recovery.in_recovery = false;
|
||||
state->recovery.recovery_start_ts = 0;
|
||||
state->recovery.recovery_duration_ms = 500; // 500ms default recovery time
|
||||
|
||||
// Protect antenna 0 by default (needed for SSB/PSS/SSS)
|
||||
state->config.protect_antenna0 = true;
|
||||
|
||||
LOG_I(PHY, "[ANTENNA_STATE] Initialized: TX antennas=%d (mask=0x%02X, array=%s), "
|
||||
"RX antennas=%d (mask=0x%02X, array=%s), antenna0_protected=%d\n",
|
||||
state->num_tx_antennas, state->tx_antenna_bitmask, state->tx_array_name,
|
||||
state->num_rx_antennas, state->rx_antenna_bitmask, state->rx_array_name,
|
||||
state->config.protect_antenna0);
|
||||
}
|
||||
|
||||
void nr_free_antenna_state(nr_antenna_state_t *state)
|
||||
{
|
||||
if (state == NULL) return;
|
||||
pthread_mutex_destroy(&state->lock);
|
||||
}
|
||||
|
||||
uint8_t nr_set_tx_antenna_bitmask(nr_antenna_state_t *state, uint8_t bitmask)
|
||||
{
|
||||
if (state == NULL) return 0;
|
||||
|
||||
pthread_mutex_lock(&state->lock);
|
||||
|
||||
uint8_t old_bitmask = state->tx_antenna_bitmask;
|
||||
|
||||
// Mask to only valid antenna bits
|
||||
uint8_t valid_mask = (1 << state->num_tx_antennas) - 1;
|
||||
state->tx_antenna_bitmask = bitmask & valid_mask;
|
||||
|
||||
// Update statistics
|
||||
if (state->tx_antenna_bitmask != old_bitmask) {
|
||||
state->stats.tx_mute_events++;
|
||||
state->stats.last_update_timestamp = get_timestamp_us();
|
||||
|
||||
LOG_I(PHY, "[ANTENNA_STATE] TX bitmask changed: 0x%02X -> 0x%02X (array=%s, active=%d/%d)\n",
|
||||
old_bitmask, state->tx_antenna_bitmask, state->tx_array_name,
|
||||
popcount8(state->tx_antenna_bitmask), state->num_tx_antennas);
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&state->lock);
|
||||
|
||||
return old_bitmask;
|
||||
}
|
||||
|
||||
uint8_t nr_set_rx_antenna_bitmask(nr_antenna_state_t *state, uint8_t bitmask)
|
||||
{
|
||||
if (state == NULL) return 0;
|
||||
|
||||
pthread_mutex_lock(&state->lock);
|
||||
|
||||
uint8_t old_bitmask = state->rx_antenna_bitmask;
|
||||
|
||||
// Mask to only valid antenna bits
|
||||
uint8_t valid_mask = (1 << state->num_rx_antennas) - 1;
|
||||
state->rx_antenna_bitmask = bitmask & valid_mask;
|
||||
|
||||
// Update statistics
|
||||
if (state->rx_antenna_bitmask != old_bitmask) {
|
||||
state->stats.rx_mute_events++;
|
||||
state->stats.last_update_timestamp = get_timestamp_us();
|
||||
|
||||
LOG_I(PHY, "[ANTENNA_STATE] RX bitmask changed: 0x%02X -> 0x%02X (array=%s, active=%d/%d)\n",
|
||||
old_bitmask, state->rx_antenna_bitmask, state->rx_array_name,
|
||||
popcount8(state->rx_antenna_bitmask), state->num_rx_antennas);
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&state->lock);
|
||||
|
||||
return old_bitmask;
|
||||
}
|
||||
|
||||
uint8_t nr_get_tx_antenna_bitmask(const nr_antenna_state_t *state)
|
||||
{
|
||||
if (state == NULL) return 0xFF;
|
||||
pthread_mutex_lock((pthread_mutex_t*)&state->lock);
|
||||
uint8_t m = state->tx_antenna_bitmask;
|
||||
pthread_mutex_unlock((pthread_mutex_t*)&state->lock);
|
||||
return m;
|
||||
}
|
||||
|
||||
uint8_t nr_get_rx_antenna_bitmask(const nr_antenna_state_t *state)
|
||||
{
|
||||
if (state == NULL) return 0xFF;
|
||||
pthread_mutex_lock((pthread_mutex_t*)&state->lock);
|
||||
uint8_t m = state->rx_antenna_bitmask;
|
||||
pthread_mutex_unlock((pthread_mutex_t*)&state->lock);
|
||||
return m;
|
||||
}
|
||||
|
||||
bool nr_is_tx_antenna_active(const nr_antenna_state_t *state, uint8_t ant_id)
|
||||
{
|
||||
if (state == NULL || ant_id >= state->num_tx_antennas) return false;
|
||||
return (state->tx_antenna_bitmask & (1 << ant_id)) != 0;
|
||||
}
|
||||
|
||||
bool nr_is_rx_antenna_active(const nr_antenna_state_t *state, uint8_t ant_id)
|
||||
{
|
||||
if (state == NULL || ant_id >= state->num_rx_antennas) return false;
|
||||
return (state->rx_antenna_bitmask & (1 << ant_id)) != 0;
|
||||
}
|
||||
|
||||
int nr_get_active_tx_antenna_count(const nr_antenna_state_t *state)
|
||||
{
|
||||
if (state == NULL) return 0;
|
||||
return popcount8(state->tx_antenna_bitmask);
|
||||
}
|
||||
|
||||
int nr_get_active_rx_antenna_count(const nr_antenna_state_t *state)
|
||||
{
|
||||
if (state == NULL) return 0;
|
||||
return popcount8(state->rx_antenna_bitmask);
|
||||
}
|
||||
|
||||
void nr_mute_tx_antenna(nr_antenna_state_t *state, uint8_t ant_id)
|
||||
{
|
||||
if (state == NULL || ant_id >= state->num_tx_antennas) return;
|
||||
|
||||
// Protect antenna 0 if configured (needed for SSB/PSS/SSS transmission)
|
||||
if (ant_id == 0 && state->config.protect_antenna0) {
|
||||
LOG_W(PHY, "[ANTENNA_STATE] Cannot mute TX antenna 0: protected for SSB transmission\n");
|
||||
LOG_W(PHY, "[ANTENNA_STATE] Use 'SET TX MASK' to override protection if needed\n");
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t new_mask = state->tx_antenna_bitmask & ~(1 << ant_id);
|
||||
nr_set_tx_antenna_bitmask(state, new_mask);
|
||||
}
|
||||
|
||||
void nr_unmute_tx_antenna(nr_antenna_state_t *state, uint8_t ant_id)
|
||||
{
|
||||
if (state == NULL || ant_id >= state->num_tx_antennas) return;
|
||||
|
||||
uint8_t new_mask = state->tx_antenna_bitmask | (1 << ant_id);
|
||||
nr_set_tx_antenna_bitmask(state, new_mask);
|
||||
}
|
||||
|
||||
void nr_mute_rx_antenna(nr_antenna_state_t *state, uint8_t ant_id)
|
||||
{
|
||||
if (state == NULL || ant_id >= state->num_rx_antennas) return;
|
||||
|
||||
uint8_t new_mask = state->rx_antenna_bitmask & ~(1 << ant_id);
|
||||
nr_set_rx_antenna_bitmask(state, new_mask);
|
||||
}
|
||||
|
||||
void nr_unmute_rx_antenna(nr_antenna_state_t *state, uint8_t ant_id)
|
||||
{
|
||||
if (state == NULL || ant_id >= state->num_rx_antennas) return;
|
||||
|
||||
uint8_t new_mask = state->rx_antenna_bitmask | (1 << ant_id);
|
||||
nr_set_rx_antenna_bitmask(state, new_mask);
|
||||
}
|
||||
|
||||
int nr_get_effective_mimo_layers(const nr_antenna_state_t *state, int requested_layers)
|
||||
{
|
||||
if (state == NULL)
|
||||
return requested_layers;
|
||||
|
||||
// Reduce MIMO layers to match the number of active TX antennas.
|
||||
// When antennas are muted, the RU blanks the corresponding TX buffers.
|
||||
// If the scheduler still uses full layer count, the precoded signal is
|
||||
// distributed across all antenna ports (including muted ones), which
|
||||
// corrupts the UE's MIMO reception. By reducing layers to match active
|
||||
// antennas, the PMI fallback to identity precoding maps layers 1:1 to
|
||||
// active ports, avoiding signal loss on muted ports.
|
||||
int active = nr_get_active_tx_antenna_count(state);
|
||||
if (active > 0 && active < requested_layers)
|
||||
return active;
|
||||
return requested_layers;
|
||||
}
|
||||
|
||||
void nr_log_antenna_state(const nr_antenna_state_t *state)
|
||||
{
|
||||
if (state == NULL) {
|
||||
LOG_I(PHY, "[ANTENNA_STATE] State is NULL\n");
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_I(PHY, "[ANTENNA_STATE] === Antenna State Report ===\n");
|
||||
LOG_I(PHY, "[ANTENNA_STATE] TX Array: %s\n", state->tx_array_name);
|
||||
LOG_I(PHY, "[ANTENNA_STATE] Configured: %d antennas\n", state->num_tx_antennas);
|
||||
LOG_I(PHY, "[ANTENNA_STATE] Bitmask: 0x%02X\n", state->tx_antenna_bitmask);
|
||||
LOG_I(PHY, "[ANTENNA_STATE] Active: %d antennas\n", nr_get_active_tx_antenna_count(state));
|
||||
LOG_I(PHY, "[ANTENNA_STATE] Individual: ");
|
||||
for (int i = 0; i < state->num_tx_antennas; i++) {
|
||||
LOG_I(PHY, "TX%d=%s ", i, nr_is_tx_antenna_active(state, i) ? "ON" : "OFF");
|
||||
}
|
||||
LOG_I(PHY, "\n");
|
||||
|
||||
LOG_I(PHY, "[ANTENNA_STATE] RX Array: %s\n", state->rx_array_name);
|
||||
LOG_I(PHY, "[ANTENNA_STATE] Configured: %d antennas\n", state->num_rx_antennas);
|
||||
LOG_I(PHY, "[ANTENNA_STATE] Bitmask: 0x%02X\n", state->rx_antenna_bitmask);
|
||||
LOG_I(PHY, "[ANTENNA_STATE] Active: %d antennas\n", nr_get_active_rx_antenna_count(state));
|
||||
|
||||
LOG_I(PHY, "[ANTENNA_STATE] Statistics:\n");
|
||||
LOG_I(PHY, "[ANTENNA_STATE] TX mute events: %lu\n", state->stats.tx_mute_events);
|
||||
LOG_I(PHY, "[ANTENNA_STATE] RX mute events: %lu\n", state->stats.rx_mute_events);
|
||||
LOG_I(PHY, "[ANTENNA_STATE] Total muted slots: %lu\n", state->stats.total_muted_slots);
|
||||
LOG_I(PHY, "[ANTENNA_STATE] =============================\n");
|
||||
}
|
||||
|
||||
void nr_get_antenna_stats(const nr_antenna_state_t *state,
|
||||
uint64_t *tx_mute_events,
|
||||
uint64_t *rx_mute_events,
|
||||
uint64_t *total_muted_slots)
|
||||
{
|
||||
if (state == NULL) return;
|
||||
|
||||
if (tx_mute_events) *tx_mute_events = state->stats.tx_mute_events;
|
||||
if (rx_mute_events) *rx_mute_events = state->stats.rx_mute_events;
|
||||
if (total_muted_slots) *total_muted_slots = state->stats.total_muted_slots;
|
||||
}
|
||||
|
||||
void nr_reset_antenna_stats(nr_antenna_state_t *state)
|
||||
{
|
||||
if (state == NULL) return;
|
||||
|
||||
pthread_mutex_lock(&state->lock);
|
||||
|
||||
state->stats.tx_mute_events = 0;
|
||||
state->stats.rx_mute_events = 0;
|
||||
state->stats.total_muted_slots = 0;
|
||||
state->stats.energy_saved_estimate = 0;
|
||||
state->stats.last_update_timestamp = get_timestamp_us();
|
||||
|
||||
LOG_I(PHY, "[ANTENNA_STATE] Statistics reset\n");
|
||||
|
||||
pthread_mutex_unlock(&state->lock);
|
||||
}
|
||||
|
||||
void nr_unmute_all_tx_antennas(nr_antenna_state_t *state)
|
||||
{
|
||||
if (state == NULL) return;
|
||||
|
||||
pthread_mutex_lock(&state->lock);
|
||||
|
||||
uint8_t old_mask = state->tx_antenna_bitmask;
|
||||
uint8_t all_on = (1 << state->num_tx_antennas) - 1;
|
||||
state->tx_antenna_bitmask = all_on;
|
||||
|
||||
// Enter recovery mode if we were muted
|
||||
if (old_mask != all_on) {
|
||||
state->recovery.in_recovery = true;
|
||||
state->recovery.recovery_start_ts = get_timestamp_us();
|
||||
LOG_I(PHY, "[ANTENNA_STATE] Entering recovery mode for %u ms after unmuting TX antennas\n",
|
||||
state->recovery.recovery_duration_ms);
|
||||
}
|
||||
|
||||
state->stats.tx_mute_events++;
|
||||
state->stats.last_update_timestamp = get_timestamp_us();
|
||||
|
||||
LOG_I(PHY, "[ANTENNA_STATE] All TX antennas unmuted: 0x%02X -> 0x%02X (array=%s)\n",
|
||||
old_mask, all_on, state->tx_array_name);
|
||||
|
||||
pthread_mutex_unlock(&state->lock);
|
||||
}
|
||||
|
||||
void nr_unmute_all_rx_antennas(nr_antenna_state_t *state)
|
||||
{
|
||||
if (state == NULL) return;
|
||||
|
||||
pthread_mutex_lock(&state->lock);
|
||||
|
||||
uint8_t old_mask = state->rx_antenna_bitmask;
|
||||
uint8_t all_on = (1 << state->num_rx_antennas) - 1;
|
||||
state->rx_antenna_bitmask = all_on;
|
||||
|
||||
state->stats.rx_mute_events++;
|
||||
state->stats.last_update_timestamp = get_timestamp_us();
|
||||
|
||||
LOG_I(PHY, "[ANTENNA_STATE] All RX antennas unmuted: 0x%02X -> 0x%02X (array=%s)\n",
|
||||
old_mask, all_on, state->rx_array_name);
|
||||
|
||||
pthread_mutex_unlock(&state->lock);
|
||||
}
|
||||
|
||||
bool nr_is_in_recovery_mode(const nr_antenna_state_t *state)
|
||||
{
|
||||
if (state == NULL) return false;
|
||||
|
||||
if (!state->recovery.in_recovery) return false;
|
||||
|
||||
// Check if recovery period has elapsed
|
||||
uint64_t now = get_timestamp_us();
|
||||
uint64_t elapsed_us = now - state->recovery.recovery_start_ts;
|
||||
uint64_t duration_us = (uint64_t)state->recovery.recovery_duration_ms * 1000ULL;
|
||||
|
||||
if (elapsed_us >= duration_us) {
|
||||
// Recovery complete - cast away const for state update
|
||||
nr_antenna_state_t *s = (nr_antenna_state_t *)state;
|
||||
s->recovery.in_recovery = false;
|
||||
LOG_I(PHY, "[ANTENNA_STATE] Recovery mode complete after %lu ms\n", elapsed_us / 1000);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void nr_set_antenna0_protection(nr_antenna_state_t *state, bool protect)
|
||||
{
|
||||
if (state == NULL) return;
|
||||
|
||||
pthread_mutex_lock(&state->lock);
|
||||
state->config.protect_antenna0 = protect;
|
||||
LOG_I(PHY, "[ANTENNA_STATE] Antenna 0 protection %s\n", protect ? "enabled" : "disabled");
|
||||
pthread_mutex_unlock(&state->lock);
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
#ifndef NR_ANTENNA_STATE_H
|
||||
#define NR_ANTENNA_STATE_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <pthread.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/// Maximum number of physical antennas supported
|
||||
#define NR_MAX_ANTENNAS 8
|
||||
|
||||
/// O-RAN compliant antenna state for TX/RX arrays
|
||||
/// Mirrors o-ran-uplane-conf.yang: tx-array-antenna-mask-config
|
||||
typedef struct {
|
||||
/// Antenna bitmask per O-RAN WG4 spec: bit N = 1 means antenna N is active
|
||||
/// Corresponds to o-ran-uplane-conf:antenna-bitmask
|
||||
uint8_t tx_antenna_bitmask;
|
||||
uint8_t rx_antenna_bitmask;
|
||||
|
||||
/// Total number of configured antennas
|
||||
uint8_t num_tx_antennas;
|
||||
uint8_t num_rx_antennas;
|
||||
|
||||
/// Thread safety for runtime updates
|
||||
pthread_mutex_t lock;
|
||||
|
||||
/// Energy saving statistics (for research/monitoring)
|
||||
struct {
|
||||
uint64_t tx_mute_events; ///< Number of TX mute operations
|
||||
uint64_t rx_mute_events; ///< Number of RX mute operations
|
||||
uint64_t total_muted_slots; ///< Total slots with at least one antenna muted
|
||||
uint64_t energy_saved_estimate; ///< Estimated energy saved (arbitrary units)
|
||||
uint64_t last_update_timestamp; ///< Last update time (for O1 notifications)
|
||||
} stats;
|
||||
|
||||
/// O-RAN compliance: array name for NETCONF identification
|
||||
char tx_array_name[64];
|
||||
char rx_array_name[64];
|
||||
|
||||
/// Recovery mode: after unmuting, allow CSI to update before using full capacity
|
||||
struct {
|
||||
bool in_recovery; ///< Currently in recovery mode
|
||||
uint64_t recovery_start_ts; ///< Timestamp when recovery started (us)
|
||||
uint32_t recovery_duration_ms; ///< Recovery duration in milliseconds (default 500ms)
|
||||
} recovery;
|
||||
|
||||
/// Configuration flags
|
||||
struct {
|
||||
bool protect_antenna0; ///< Never mute antenna 0 (needed for SSB)
|
||||
} config;
|
||||
|
||||
} nr_antenna_state_t;
|
||||
|
||||
/**
|
||||
* @brief Initialize antenna state with all antennas active
|
||||
* @param state Pointer to antenna state structure
|
||||
* @param num_tx Number of TX antennas
|
||||
* @param num_rx Number of RX antennas
|
||||
* @param tx_array_name O-RAN array name for TX (e.g., "tx-array-0")
|
||||
* @param rx_array_name O-RAN array name for RX (e.g., "rx-array-0")
|
||||
*/
|
||||
void nr_init_antenna_state(nr_antenna_state_t *state,
|
||||
uint8_t num_tx,
|
||||
uint8_t num_rx,
|
||||
const char *tx_array_name,
|
||||
const char *rx_array_name);
|
||||
|
||||
/**
|
||||
* @brief Free antenna state resources
|
||||
* @param state Pointer to antenna state structure
|
||||
*/
|
||||
void nr_free_antenna_state(nr_antenna_state_t *state);
|
||||
|
||||
/**
|
||||
* @brief Set TX antenna bitmask (O-RAN O1 interface equivalent)
|
||||
*
|
||||
* This function implements the O-RAN WG4 MPLANE-TRX-CONTROL feature.
|
||||
* Corresponds to NETCONF edit-config on tx-array-antenna-mask-config.
|
||||
*
|
||||
* @param state Pointer to antenna state structure
|
||||
* @param bitmask Antenna bitmask (bit N = 1 means antenna N active)
|
||||
* @return Previous bitmask value
|
||||
*/
|
||||
uint8_t nr_set_tx_antenna_bitmask(nr_antenna_state_t *state, uint8_t bitmask);
|
||||
|
||||
/**
|
||||
* @brief Set RX antenna bitmask (O-RAN O1 interface equivalent)
|
||||
* @param state Pointer to antenna state structure
|
||||
* @param bitmask Antenna bitmask (bit N = 1 means antenna N active)
|
||||
* @return Previous bitmask value
|
||||
*/
|
||||
uint8_t nr_set_rx_antenna_bitmask(nr_antenna_state_t *state, uint8_t bitmask);
|
||||
|
||||
/**
|
||||
* @brief Get current TX antenna bitmask
|
||||
* @param state Pointer to antenna state structure
|
||||
* @return Current TX antenna bitmask
|
||||
*/
|
||||
uint8_t nr_get_tx_antenna_bitmask(const nr_antenna_state_t *state);
|
||||
|
||||
/**
|
||||
* @brief Get current RX antenna bitmask
|
||||
* @param state Pointer to antenna state structure
|
||||
* @return Current RX antenna bitmask
|
||||
*/
|
||||
uint8_t nr_get_rx_antenna_bitmask(const nr_antenna_state_t *state);
|
||||
|
||||
/**
|
||||
* @brief Check if a specific TX antenna is active
|
||||
* @param state Pointer to antenna state structure
|
||||
* @param ant_id Antenna index (0-based)
|
||||
* @return true if antenna is active, false if muted
|
||||
*/
|
||||
bool nr_is_tx_antenna_active(const nr_antenna_state_t *state, uint8_t ant_id);
|
||||
|
||||
/**
|
||||
* @brief Check if a specific RX antenna is active
|
||||
* @param state Pointer to antenna state structure
|
||||
* @param ant_id Antenna index (0-based)
|
||||
* @return true if antenna is active, false if muted
|
||||
*/
|
||||
bool nr_is_rx_antenna_active(const nr_antenna_state_t *state, uint8_t ant_id);
|
||||
|
||||
/**
|
||||
* @brief Get count of currently active TX antennas
|
||||
* @param state Pointer to antenna state structure
|
||||
* @return Number of active TX antennas
|
||||
*/
|
||||
int nr_get_active_tx_antenna_count(const nr_antenna_state_t *state);
|
||||
|
||||
/**
|
||||
* @brief Get count of currently active RX antennas
|
||||
* @param state Pointer to antenna state structure
|
||||
* @return Number of active RX antennas
|
||||
*/
|
||||
int nr_get_active_rx_antenna_count(const nr_antenna_state_t *state);
|
||||
|
||||
/**
|
||||
* @brief Mute a specific TX antenna
|
||||
* @param state Pointer to antenna state structure
|
||||
* @param ant_id Antenna index to mute (0-based)
|
||||
*/
|
||||
void nr_mute_tx_antenna(nr_antenna_state_t *state, uint8_t ant_id);
|
||||
|
||||
/**
|
||||
* @brief Unmute a specific TX antenna
|
||||
* @param state Pointer to antenna state structure
|
||||
* @param ant_id Antenna index to unmute (0-based)
|
||||
*/
|
||||
void nr_unmute_tx_antenna(nr_antenna_state_t *state, uint8_t ant_id);
|
||||
|
||||
/**
|
||||
* @brief Mute a specific RX antenna
|
||||
* @param state Pointer to antenna state structure
|
||||
* @param ant_id Antenna index to mute (0-based)
|
||||
*/
|
||||
void nr_mute_rx_antenna(nr_antenna_state_t *state, uint8_t ant_id);
|
||||
|
||||
/**
|
||||
* @brief Unmute a specific RX antenna
|
||||
* @param state Pointer to antenna state structure
|
||||
* @param ant_id Antenna index to unmute (0-based)
|
||||
*/
|
||||
void nr_unmute_rx_antenna(nr_antenna_state_t *state, uint8_t ant_id);
|
||||
|
||||
/**
|
||||
* @brief Get effective number of MIMO layers based on active antennas
|
||||
*
|
||||
* For MIMO, the number of layers is limited by active antennas.
|
||||
* This function returns min(requested_layers, active_antennas).
|
||||
*
|
||||
* @param state Pointer to antenna state structure
|
||||
* @param requested_layers Originally requested number of layers
|
||||
* @return Effective number of layers that can be used
|
||||
*/
|
||||
int nr_get_effective_mimo_layers(const nr_antenna_state_t *state, int requested_layers);
|
||||
|
||||
/**
|
||||
* @brief Log antenna state for debugging
|
||||
* @param state Pointer to antenna state structure
|
||||
*/
|
||||
void nr_log_antenna_state(const nr_antenna_state_t *state);
|
||||
|
||||
/**
|
||||
* @brief Get energy saving statistics
|
||||
* @param state Pointer to antenna state structure
|
||||
* @param tx_mute_events Output: number of TX mute events
|
||||
* @param rx_mute_events Output: number of RX mute events
|
||||
* @param total_muted_slots Output: total slots with muting
|
||||
*/
|
||||
void nr_get_antenna_stats(const nr_antenna_state_t *state,
|
||||
uint64_t *tx_mute_events,
|
||||
uint64_t *rx_mute_events,
|
||||
uint64_t *total_muted_slots);
|
||||
|
||||
/**
|
||||
* @brief Reset energy saving statistics
|
||||
* @param state Pointer to antenna state structure
|
||||
*/
|
||||
void nr_reset_antenna_stats(nr_antenna_state_t *state);
|
||||
|
||||
/**
|
||||
* @brief Unmute all TX antennas and enter recovery mode
|
||||
* @param state Pointer to antenna state structure
|
||||
*
|
||||
* This initiates a recovery period during which the scheduler
|
||||
* uses conservative settings to allow UE CSI reports to update.
|
||||
*/
|
||||
void nr_unmute_all_tx_antennas(nr_antenna_state_t *state);
|
||||
|
||||
/**
|
||||
* @brief Unmute all RX antennas
|
||||
* @param state Pointer to antenna state structure
|
||||
*/
|
||||
void nr_unmute_all_rx_antennas(nr_antenna_state_t *state);
|
||||
|
||||
/**
|
||||
* @brief Check if system is in recovery mode after unmuting
|
||||
* @param state Pointer to antenna state structure
|
||||
* @return true if in recovery mode
|
||||
*/
|
||||
bool nr_is_in_recovery_mode(const nr_antenna_state_t *state);
|
||||
|
||||
/**
|
||||
* @brief Set antenna 0 protection (SSB antenna)
|
||||
* @param state Pointer to antenna state structure
|
||||
* @param protect true to prevent muting antenna 0
|
||||
*/
|
||||
void nr_set_antenna0_protection(nr_antenna_state_t *state, bool protect);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NR_ANTENNA_STATE_H */
|
||||
|
||||
@@ -462,92 +462,117 @@ static inline void do_txdataF(c16_t **txdataF,
|
||||
int start_sc,
|
||||
int txdataF_offset_per_symbol)
|
||||
{
|
||||
NR_DL_FRAME_PARMS *frame_parms = &gNB->frame_parms;
|
||||
int rb = 0;
|
||||
uint16_t subCarrier = start_sc;
|
||||
const nfapi_nr_tx_precoding_and_beamforming_t *pb = &rel15->precodingAndBeamforming;
|
||||
while (rb < rel15->rbSize) {
|
||||
// get pmi info
|
||||
const int pmi = (pb->prg_size > 0) ? (pb->prgs_list[(int)rb / pb->prg_size].pm_idx) : 0;
|
||||
const int pmi2 = (rb < (rel15->rbSize - 1) && pb->prg_size > 0) ? (pb->prgs_list[(int)(rb + 1) / pb->prg_size].pm_idx) : -1;
|
||||
const int pmi3 = (rb < (rel15->rbSize - 2) && pb->prg_size > 0) ? (pb->prgs_list[(int)(rb + 2) / pb->prg_size].pm_idx) : -1;
|
||||
const int pmi4 = (rb < (rel15->rbSize - 3) && pb->prg_size > 0) ? (pb->prgs_list[(int)(rb + 3) / pb->prg_size].pm_idx) : -1;
|
||||
NR_DL_FRAME_PARMS *frame_parms = &gNB->frame_parms;
|
||||
int rb = 0;
|
||||
uint16_t subCarrier = start_sc;
|
||||
const nfapi_nr_tx_precoding_and_beamforming_t *pb = &rel15->precodingAndBeamforming;
|
||||
|
||||
// If pmi of next RB and pmi of current RB are the same, we do 2 RB in a row
|
||||
// if pmi differs, or current rb is the end (rel15->rbSize - 1), than we do 1 RB in a row
|
||||
int rb_step0 = pmi == pmi2 ? 2 : 1;
|
||||
const int rb_step = rb_step0==2 && pmi3==pmi && pmi4==pmi ? 4 : rb_step0;
|
||||
const int re_cnt = NR_NB_SC_PER_RB * rb_step;
|
||||
if (pmi == 0) { // unitary Precoding
|
||||
if (subCarrier + re_cnt <= symbol_sz) { // RB does not cross DC
|
||||
if (ant < rel15->nrOfLayers)
|
||||
memcpy(&txdataF[ant][txdataF_offset_per_symbol + subCarrier],
|
||||
&txdataF_precoding[ant][subCarrier],
|
||||
re_cnt * sizeof(**txdataF));
|
||||
else
|
||||
memset(&txdataF[ant][txdataF_offset_per_symbol + subCarrier], 0, re_cnt * sizeof(**txdataF));
|
||||
} else { // RB does cross DC
|
||||
const int neg_length = symbol_sz - subCarrier;
|
||||
const int pos_length = re_cnt - neg_length;
|
||||
if (ant < rel15->nrOfLayers) {
|
||||
memcpy(&txdataF[ant][txdataF_offset_per_symbol + subCarrier],
|
||||
&txdataF_precoding[ant][subCarrier],
|
||||
neg_length * sizeof(**txdataF));
|
||||
memcpy(&txdataF[ant][txdataF_offset_per_symbol], &txdataF_precoding[ant], pos_length * sizeof(**txdataF));
|
||||
// Compute which layer this antenna carries under identity precoding.
|
||||
// Standard identity maps layer N → antenna N, but when antennas are muted
|
||||
// we must remap layers to active antennas. Otherwise layer data sent to a
|
||||
// muted antenna is lost (RU blanks it) while active antennas sit idle.
|
||||
// E.g. mask=0x0D (ant 1 muted, 3 layers): ant0→L0, ant2→L1, ant3→L2.
|
||||
int identity_layer = -1;
|
||||
{
|
||||
const nr_antenna_state_t *ast = &gNB->antenna_state;
|
||||
int active_count = nr_get_active_tx_antenna_count(ast);
|
||||
if (active_count > 0 && active_count < frame_parms->nb_antennas_tx) {
|
||||
if (nr_is_tx_antenna_active(ast, ant)) {
|
||||
identity_layer = 0;
|
||||
for (int a = 0; a < ant; a++)
|
||||
if (nr_is_tx_antenna_active(ast, a))
|
||||
identity_layer++;
|
||||
if (identity_layer >= rel15->nrOfLayers)
|
||||
identity_layer = -1;
|
||||
}
|
||||
} else {
|
||||
memset(&txdataF[ant][txdataF_offset_per_symbol + subCarrier], 0, neg_length * sizeof(**txdataF));
|
||||
memset(&txdataF[ant][txdataF_offset_per_symbol], 0, pos_length * sizeof(**txdataF));
|
||||
// No muting: standard identity (layer N = antenna N)
|
||||
identity_layer = (ant < rel15->nrOfLayers) ? ant : -1;
|
||||
}
|
||||
}
|
||||
subCarrier += re_cnt;
|
||||
if (subCarrier >= symbol_sz) {
|
||||
subCarrier -= symbol_sz;
|
||||
}
|
||||
} else { // non-unitary Precoding
|
||||
AssertFatal(frame_parms->nb_antennas_tx > 1, "No precoding can be done with a single antenna port\n");
|
||||
// get the precoding matrix weights:
|
||||
nfapi_nr_pm_pdu_t *pmi_pdu = &gNB->gNB_config.pmi_list.pmi_pdu[pmi - 1]; // pmi 0 is identity matrix
|
||||
AssertFatal(pmi == pmi_pdu->pm_idx, "PMI %d doesn't match to the one in precoding matrix %d\n", pmi, pmi_pdu->pm_idx);
|
||||
AssertFatal(ant < pmi_pdu->num_ant_ports,
|
||||
"Antenna port index %d exceeds precoding matrix AP size %d\n",
|
||||
ant,
|
||||
pmi_pdu->num_ant_ports);
|
||||
AssertFatal(rel15->nrOfLayers == pmi_pdu->numLayers,
|
||||
"Number of layers %d doesn't match to the one in precoding matrix %d\n",
|
||||
rel15->nrOfLayers,
|
||||
pmi_pdu->numLayers);
|
||||
if ((subCarrier + re_cnt) < symbol_sz) { // within ofdm_symbol_size, use SIMDe
|
||||
nr_layer_precoder_simd(rel15->nrOfLayers,
|
||||
symbol_sz,
|
||||
txdataF_precoding,
|
||||
ant,
|
||||
pmi_pdu,
|
||||
subCarrier,
|
||||
re_cnt,
|
||||
&txdataF[ant][txdataF_offset_per_symbol]);
|
||||
subCarrier += re_cnt;
|
||||
} else { // crossing ofdm_symbol_size, use simple arithmetic operations
|
||||
for (int i = 0; i < re_cnt; i++) {
|
||||
txdataF[ant][txdataF_offset_per_symbol + subCarrier] =
|
||||
nr_layer_precoder_cm(rel15->nrOfLayers, symbol_sz, txdataF_precoding, ant, pmi_pdu, subCarrier);
|
||||
#ifdef DEBUG_DLSCH_MAPPING
|
||||
printf("antenna %d\t l %d \t subCarrier %d \t txdataF: %d %d\n",
|
||||
ant,
|
||||
l_symbol,
|
||||
subCarrier,
|
||||
txdataF[ant][l_symbol * symbol_sz + subCarrier + txdataF_offset].r,
|
||||
txdataF[ant][l_symbol * symbol_sz + subCarrier + txdataF_offset].i);
|
||||
#endif
|
||||
if (++subCarrier >= symbol_sz) {
|
||||
subCarrier -= symbol_sz;
|
||||
}
|
||||
}
|
||||
} // else{ // crossing ofdm_symbol_size, use simple arithmetic operations
|
||||
} // else { // non-unitary Precoding
|
||||
}
|
||||
|
||||
rb += rb_step;
|
||||
} // RB loop: while(rb < rel15->rbSize)
|
||||
while (rb < rel15->rbSize) {
|
||||
// get pmi info
|
||||
const int pmi = (pb->prg_size > 0) ? (pb->prgs_list[(int)rb / pb->prg_size].pm_idx) : 0;
|
||||
const int pmi2 = (rb < (rel15->rbSize - 1) && pb->prg_size > 0) ? (pb->prgs_list[(int)(rb + 1) / pb->prg_size].pm_idx) : -1;
|
||||
const int pmi3 = (rb < (rel15->rbSize - 2) && pb->prg_size > 0) ? (pb->prgs_list[(int)(rb + 2) / pb->prg_size].pm_idx) : -1;
|
||||
const int pmi4 = (rb < (rel15->rbSize - 3) && pb->prg_size > 0) ? (pb->prgs_list[(int)(rb + 3) / pb->prg_size].pm_idx) : -1;
|
||||
|
||||
// If pmi of next RB and pmi of current RB are the same, we do 2 RB in a row
|
||||
// if pmi differs, or current rb is the end (rel15->rbSize - 1), than we do 1 RB in a row
|
||||
int rb_step0 = pmi == pmi2 ? 2 : 1;
|
||||
const int rb_step = rb_step0==2 && pmi3==pmi && pmi4==pmi ? 4 : rb_step0;
|
||||
const int re_cnt = NR_NB_SC_PER_RB * rb_step;
|
||||
if (pmi == 0) { // unitary Precoding
|
||||
if (subCarrier + re_cnt <= symbol_sz) { // RB does not cross DC
|
||||
if (identity_layer >= 0)
|
||||
memcpy(&txdataF[ant][txdataF_offset_per_symbol + subCarrier],
|
||||
&txdataF_precoding[identity_layer][subCarrier],
|
||||
re_cnt * sizeof(**txdataF));
|
||||
else
|
||||
memset(&txdataF[ant][txdataF_offset_per_symbol + subCarrier], 0, re_cnt * sizeof(**txdataF));
|
||||
} else { // RB does cross DC
|
||||
const int neg_length = symbol_sz - subCarrier;
|
||||
const int pos_length = re_cnt - neg_length;
|
||||
if (identity_layer >= 0) {
|
||||
memcpy(&txdataF[ant][txdataF_offset_per_symbol + subCarrier],
|
||||
&txdataF_precoding[identity_layer][subCarrier],
|
||||
neg_length * sizeof(**txdataF));
|
||||
memcpy(&txdataF[ant][txdataF_offset_per_symbol], &txdataF_precoding[identity_layer], pos_length * sizeof(**txdataF));
|
||||
} else {
|
||||
memset(&txdataF[ant][txdataF_offset_per_symbol + subCarrier], 0, neg_length * sizeof(**txdataF));
|
||||
memset(&txdataF[ant][txdataF_offset_per_symbol], 0, pos_length * sizeof(**txdataF));
|
||||
}
|
||||
}
|
||||
subCarrier += re_cnt;
|
||||
if (subCarrier >= symbol_sz) {
|
||||
subCarrier -= symbol_sz;
|
||||
}
|
||||
} else { // non-unitary Precoding
|
||||
AssertFatal(frame_parms->nb_antennas_tx > 1, "No precoding can be done with a single antenna port\n");
|
||||
// get the precoding matrix weights:
|
||||
nfapi_nr_pm_pdu_t *pmi_pdu = &gNB->gNB_config.pmi_list.pmi_pdu[pmi - 1]; // pmi 0 is identity matrix
|
||||
AssertFatal(pmi == pmi_pdu->pm_idx, "PMI %d doesn't match to the one in precoding matrix %d\n", pmi, pmi_pdu->pm_idx);
|
||||
AssertFatal(ant < pmi_pdu->num_ant_ports,
|
||||
"Antenna port index %d exceeds precoding matrix AP size %d\n",
|
||||
ant,
|
||||
pmi_pdu->num_ant_ports);
|
||||
AssertFatal(rel15->nrOfLayers == pmi_pdu->numLayers,
|
||||
"Number of layers %d doesn't match to the one in precoding matrix %d\n",
|
||||
rel15->nrOfLayers,
|
||||
pmi_pdu->numLayers);
|
||||
if ((subCarrier + re_cnt) < symbol_sz) { // within ofdm_symbol_size, use SIMDe
|
||||
nr_layer_precoder_simd(rel15->nrOfLayers,
|
||||
symbol_sz,
|
||||
txdataF_precoding,
|
||||
ant,
|
||||
pmi_pdu,
|
||||
subCarrier,
|
||||
re_cnt,
|
||||
&txdataF[ant][txdataF_offset_per_symbol]);
|
||||
subCarrier += re_cnt;
|
||||
} else { // crossing ofdm_symbol_size, use simple arithmetic operations
|
||||
for (int i = 0; i < re_cnt; i++) {
|
||||
txdataF[ant][txdataF_offset_per_symbol + subCarrier] =
|
||||
nr_layer_precoder_cm(rel15->nrOfLayers, symbol_sz, txdataF_precoding, ant, pmi_pdu, subCarrier);
|
||||
#ifdef DEBUG_DLSCH_MAPPING
|
||||
printf("antenna %d\t l %d \t subCarrier %d \t txdataF: %d %d\n",
|
||||
ant,
|
||||
l_symbol,
|
||||
subCarrier,
|
||||
txdataF[ant][l_symbol * symbol_sz + subCarrier + txdataF_offset].r,
|
||||
txdataF[ant][l_symbol * symbol_sz + subCarrier + txdataF_offset].i);
|
||||
#endif
|
||||
if (++subCarrier >= symbol_sz) {
|
||||
subCarrier -= symbol_sz;
|
||||
}
|
||||
}
|
||||
} // else{ // crossing ofdm_symbol_size, use simple arithmetic operations
|
||||
} // else { // non-unitary Precoding
|
||||
rb += rb_step;
|
||||
} // RB loop: while(rb < rel15->rbSize)
|
||||
}
|
||||
|
||||
static int do_one_dlsch(unsigned char *input_ptr, PHY_VARS_gNB *gNB, NR_gNB_DLSCH_t *dlsch, int slot)
|
||||
{
|
||||
const int16_t amp = gNB->TX_AMP;
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
|
||||
#include "executables/nr-softmodem-common.h"
|
||||
#include "nr_transport_proto_ue.h"
|
||||
@@ -118,6 +119,103 @@ void nr_numer_2x2(int32_t *a_00_sq,
|
||||
}
|
||||
}
|
||||
|
||||
// Compute eigenvalues of an NxN Hermitian matrix (N <= 4) using the Jacobi method.
|
||||
// The matrix is provided as separate real and imaginary float arrays.
|
||||
// Eigenvalues are returned sorted in descending order.
|
||||
static void nr_hermitian_eigenvalues(const int N, float A_re[4][4], float A_im[4][4], float eigenvalues[4])
|
||||
{
|
||||
for (int sweep = 0; sweep < 30; sweep++) {
|
||||
float off_norm = 0;
|
||||
for (int i = 0; i < N; i++)
|
||||
for (int j = i + 1; j < N; j++)
|
||||
off_norm += A_re[i][j] * A_re[i][j] + A_im[i][j] * A_im[i][j];
|
||||
if (off_norm < 1e-10f)
|
||||
break;
|
||||
|
||||
for (int p = 0; p < N - 1; p++) {
|
||||
for (int q = p + 1; q < N; q++) {
|
||||
float re_pq = A_re[p][q], im_pq = A_im[p][q];
|
||||
float mag = sqrtf(re_pq * re_pq + im_pq * im_pq);
|
||||
if (mag < 1e-12f)
|
||||
continue;
|
||||
|
||||
// Phase elimination: make A[p][q] real by rotating column/row q
|
||||
float cos_phi = re_pq / mag;
|
||||
float sin_phi = im_pq / mag;
|
||||
|
||||
// Similarity transformation D^H A D with D=diag(1,..,e^{-jphi},..,1)
|
||||
// Column q: A[k][q] *= e^{-jphi} for k != q (diagonal unchanged)
|
||||
for (int k = 0; k < N; k++) {
|
||||
if (k == q) continue;
|
||||
float old_re = A_re[k][q], old_im = A_im[k][q];
|
||||
A_re[k][q] = old_re * cos_phi + old_im * sin_phi;
|
||||
A_im[k][q] = -old_re * sin_phi + old_im * cos_phi;
|
||||
}
|
||||
// Row q from Hermitian symmetry: A[q][k] = conj(A[k][q])
|
||||
for (int k = 0; k < N; k++) {
|
||||
if (k == q)
|
||||
continue;
|
||||
A_re[q][k] = A_re[k][q];
|
||||
A_im[q][k] = -A_im[k][q];
|
||||
}
|
||||
|
||||
// Now A[p][q] is real and positive (= mag). Apply real Jacobi rotation.
|
||||
float d = A_re[q][q] - A_re[p][p];
|
||||
float t;
|
||||
if (fabsf(d) < 1e-12f) {
|
||||
t = 1.0f;
|
||||
} else {
|
||||
float tau = d / (2.0f * mag);
|
||||
t = ((tau >= 0) ? 1.0f : -1.0f) / (fabsf(tau) + sqrtf(1.0f + tau * tau));
|
||||
}
|
||||
|
||||
float c = 1.0f / sqrtf(1.0f + t * t);
|
||||
float s = t * c;
|
||||
|
||||
// Update diagonal
|
||||
A_re[p][p] -= t * mag;
|
||||
A_re[q][q] += t * mag;
|
||||
|
||||
// Zero out (p,q) and (q,p)
|
||||
A_re[p][q] = 0;
|
||||
A_im[p][q] = 0;
|
||||
A_re[q][p] = 0;
|
||||
A_im[q][p] = 0;
|
||||
|
||||
// Update remaining off-diagonal elements
|
||||
for (int r = 0; r < N; r++) {
|
||||
if (r == p || r == q)
|
||||
continue;
|
||||
float rp_re = A_re[r][p], rp_im = A_im[r][p];
|
||||
float rq_re = A_re[r][q], rq_im = A_im[r][q];
|
||||
|
||||
A_re[r][p] = c * rp_re - s * rq_re;
|
||||
A_im[r][p] = c * rp_im - s * rq_im;
|
||||
A_re[p][r] = A_re[r][p];
|
||||
A_im[p][r] = -A_im[r][p];
|
||||
|
||||
A_re[r][q] = s * rp_re + c * rq_re;
|
||||
A_im[r][q] = s * rp_im + c * rq_im;
|
||||
A_re[q][r] = A_re[r][q];
|
||||
A_im[q][r] = -A_im[r][q];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < N; i++)
|
||||
eigenvalues[i] = A_re[i][i] > 0.0f ? A_re[i][i] : 0.0f;
|
||||
|
||||
// Sort descending
|
||||
for (int i = 0; i < N - 1; i++)
|
||||
for (int j = i + 1; j < N; j++)
|
||||
if (eigenvalues[j] > eigenvalues[i]) {
|
||||
float tmp = eigenvalues[i];
|
||||
eigenvalues[i] = eigenvalues[j];
|
||||
eigenvalues[j] = tmp;
|
||||
}
|
||||
}
|
||||
|
||||
bool is_csi_rs_in_symbol(const fapi_nr_dl_config_csirs_pdu_rel15_t csirs_config_pdu, const int symbol) {
|
||||
|
||||
bool ret = false;
|
||||
@@ -450,25 +548,38 @@ int nr_csi_rs_ri_estimation(const PHY_VARS_NR_UE *ue,
|
||||
|
||||
if (ue->frame_parms.nb_antennas_rx == 1 || N_ports == 1) {
|
||||
return 0;
|
||||
} else if( !(ue->frame_parms.nb_antennas_rx == 2 && N_ports == 2) ) {
|
||||
}
|
||||
|
||||
const int max_rank = N_ports < frame_parms->nb_antennas_rx ? N_ports : frame_parms->nb_antennas_rx;
|
||||
if (max_rank < 2) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (N_ports > 4 || frame_parms->nb_antennas_rx > 4) {
|
||||
LOG_W(NR_PHY, "Rank indicator computation is not implemented for %i x %i system\n",
|
||||
ue->frame_parms.nb_antennas_rx, N_ports);
|
||||
frame_parms->nb_antennas_rx, N_ports);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Example 2x2: Hh x H =
|
||||
* | conjch00 conjch10 | x | ch00 ch01 | = | conjch00*ch00+conjch10*ch10 conjch00*ch01+conjch10*ch11 |
|
||||
* | conjch01 conjch11 | | ch10 ch11 | | conjch01*ch00+conjch11*ch10 conjch01*ch01+conjch11*ch11 |
|
||||
*/
|
||||
/* Hh x H = Gram matrix computation (general for any N_ports x nb_antennas_rx) */
|
||||
|
||||
c16_t csi_rs_estimated_conjch_ch[frame_parms->nb_antennas_rx][N_ports][frame_parms->nb_antennas_rx][N_ports]
|
||||
[frame_parms->ofdm_symbol_size + FILTER_MARGIN] __attribute__((aligned(32)));
|
||||
int32_t csi_rs_estimated_A_MF[N_ports][N_ports][frame_parms->ofdm_symbol_size + FILTER_MARGIN] __attribute__((aligned(32)));
|
||||
memset(csi_rs_estimated_A_MF, 0, sizeof(csi_rs_estimated_A_MF));
|
||||
|
||||
// Arrays only needed for the 2x2 fast path
|
||||
int32_t csi_rs_estimated_A_MF_sq[N_ports][N_ports][frame_parms->ofdm_symbol_size + FILTER_MARGIN] __attribute__((aligned(32)));
|
||||
int32_t csi_rs_estimated_determ_fin[frame_parms->ofdm_symbol_size + FILTER_MARGIN] __attribute__((aligned(32)));
|
||||
int32_t csi_rs_estimated_numer_fin[frame_parms->ofdm_symbol_size + FILTER_MARGIN] __attribute__((aligned(32)));
|
||||
|
||||
// For 4x4: eigenvalue-based rank voting. rank_count[r] = number of subcarriers supporting rank >= r+1
|
||||
int rank_count[4] = {0};
|
||||
// For 2x2: keep existing condition number approach
|
||||
const uint8_t sum_shift = 1; // log2(2x2) = 2, which is a shift of 1 bit
|
||||
|
||||
// Eigenvalue ratio threshold in dB (approx. equivalent to cond_dB_threshold=5 for 2x2)
|
||||
const float eig_ratio_dB_threshold = 8.0f;
|
||||
|
||||
for (int rb = csirs_config_pdu->start_rb; rb < (csirs_config_pdu->start_rb+csirs_config_pdu->nr_of_rbs); rb++) {
|
||||
|
||||
if (csirs_config_pdu->freq_density <= 1 && csirs_config_pdu->freq_density != (rb % 2)) {
|
||||
@@ -477,6 +588,7 @@ int nr_csi_rs_ri_estimation(const PHY_VARS_NR_UE *ue,
|
||||
uint16_t k = (frame_parms->first_carrier_offset + rb*NR_NB_SC_PER_RB) % frame_parms->ofdm_symbol_size;
|
||||
uint16_t k_offset = k + mem_offset;
|
||||
|
||||
// Compute Gram matrix A = H^H * H (general for any N_ports)
|
||||
for (int ant_rx_conjch = 0; ant_rx_conjch < frame_parms->nb_antennas_rx; ant_rx_conjch++) {
|
||||
for(uint16_t port_tx_conjch = 0; port_tx_conjch < N_ports; port_tx_conjch++) {
|
||||
for (int ant_rx_ch = 0; ant_rx_ch < frame_parms->nb_antennas_rx; ant_rx_ch++) {
|
||||
@@ -499,68 +611,119 @@ int nr_csi_rs_ri_estimation(const PHY_VARS_NR_UE *ue,
|
||||
}
|
||||
}
|
||||
|
||||
// compute the determinant of A_MF (denominator)
|
||||
nr_det_A_MF_2x2(&csi_rs_estimated_A_MF[0][0][k_offset],
|
||||
&csi_rs_estimated_A_MF[0][1][k_offset],
|
||||
&csi_rs_estimated_A_MF[1][0][k_offset],
|
||||
&csi_rs_estimated_A_MF[1][1][k_offset],
|
||||
&csi_rs_estimated_determ_fin[k_offset],
|
||||
1);
|
||||
if (N_ports == 2) {
|
||||
// ---- 2x2 fast path: existing SIMD-based condition number approach ----
|
||||
nr_det_A_MF_2x2(&csi_rs_estimated_A_MF[0][0][k_offset],
|
||||
&csi_rs_estimated_A_MF[0][1][k_offset],
|
||||
&csi_rs_estimated_A_MF[1][0][k_offset],
|
||||
&csi_rs_estimated_A_MF[1][1][k_offset],
|
||||
&csi_rs_estimated_determ_fin[k_offset],
|
||||
1);
|
||||
|
||||
// compute the square of A_MF (numerator)
|
||||
nr_squared_matrix_element(&csi_rs_estimated_A_MF[0][0][k_offset], &csi_rs_estimated_A_MF_sq[0][0][k_offset], 1);
|
||||
nr_squared_matrix_element(&csi_rs_estimated_A_MF[0][1][k_offset], &csi_rs_estimated_A_MF_sq[0][1][k_offset], 1);
|
||||
nr_squared_matrix_element(&csi_rs_estimated_A_MF[1][0][k_offset], &csi_rs_estimated_A_MF_sq[1][0][k_offset], 1);
|
||||
nr_squared_matrix_element(&csi_rs_estimated_A_MF[1][1][k_offset], &csi_rs_estimated_A_MF_sq[1][1][k_offset], 1);
|
||||
nr_numer_2x2(&csi_rs_estimated_A_MF_sq[0][0][k_offset],
|
||||
&csi_rs_estimated_A_MF_sq[0][1][k_offset],
|
||||
&csi_rs_estimated_A_MF_sq[1][0][k_offset],
|
||||
&csi_rs_estimated_A_MF_sq[1][1][k_offset],
|
||||
&csi_rs_estimated_numer_fin[k_offset],
|
||||
1);
|
||||
nr_squared_matrix_element(&csi_rs_estimated_A_MF[0][0][k_offset], &csi_rs_estimated_A_MF_sq[0][0][k_offset], 1);
|
||||
nr_squared_matrix_element(&csi_rs_estimated_A_MF[0][1][k_offset], &csi_rs_estimated_A_MF_sq[0][1][k_offset], 1);
|
||||
nr_squared_matrix_element(&csi_rs_estimated_A_MF[1][0][k_offset], &csi_rs_estimated_A_MF_sq[1][0][k_offset], 1);
|
||||
nr_squared_matrix_element(&csi_rs_estimated_A_MF[1][1][k_offset], &csi_rs_estimated_A_MF_sq[1][1][k_offset], 1);
|
||||
nr_numer_2x2(&csi_rs_estimated_A_MF_sq[0][0][k_offset],
|
||||
&csi_rs_estimated_A_MF_sq[0][1][k_offset],
|
||||
&csi_rs_estimated_A_MF_sq[1][0][k_offset],
|
||||
&csi_rs_estimated_A_MF_sq[1][1][k_offset],
|
||||
&csi_rs_estimated_numer_fin[k_offset],
|
||||
1);
|
||||
|
||||
#ifdef NR_CSIRS_DEBUG
|
||||
for(uint16_t port_tx_conjch = 0; port_tx_conjch < N_ports; port_tx_conjch++) {
|
||||
for(uint16_t port_tx_ch = 0; port_tx_ch < N_ports; port_tx_ch++) {
|
||||
c16_t *csi_rs_estimated_A_MF_k = (c16_t *) &csi_rs_estimated_A_MF[port_tx_conjch][port_tx_ch][k_offset];
|
||||
LOG_I(NR_PHY, "(%i) csi_rs_estimated_A_MF[%i][%i] = (%i, %i)\n",
|
||||
k, port_tx_conjch, port_tx_ch, csi_rs_estimated_A_MF_k->r, csi_rs_estimated_A_MF_k->i);
|
||||
c16_t *csi_rs_estimated_A_MF_sq_k = (c16_t *) &csi_rs_estimated_A_MF_sq[port_tx_conjch][port_tx_ch][k_offset];
|
||||
LOG_I(NR_PHY, "(%i) csi_rs_estimated_A_MF_sq[%i][%i] = (%i, %i)\n",
|
||||
k, port_tx_conjch, port_tx_ch, csi_rs_estimated_A_MF_sq_k->r, csi_rs_estimated_A_MF_sq_k->i);
|
||||
for(uint16_t port_tx_conjch = 0; port_tx_conjch < N_ports; port_tx_conjch++) {
|
||||
for(uint16_t port_tx_ch = 0; port_tx_ch < N_ports; port_tx_ch++) {
|
||||
c16_t *csi_rs_estimated_A_MF_k = (c16_t *) &csi_rs_estimated_A_MF[port_tx_conjch][port_tx_ch][k_offset];
|
||||
LOG_I(NR_PHY, "(%i) csi_rs_estimated_A_MF[%i][%i] = (%i, %i)\n",
|
||||
k, port_tx_conjch, port_tx_ch, csi_rs_estimated_A_MF_k->r, csi_rs_estimated_A_MF_k->i);
|
||||
}
|
||||
}
|
||||
}
|
||||
LOG_I(NR_PHY, "(%i) csi_rs_estimated_determ_fin = %i\n", k, csi_rs_estimated_determ_fin[k_offset]);
|
||||
LOG_I(NR_PHY, "(%i) csi_rs_estimated_numer_fin = %i\n", k, csi_rs_estimated_numer_fin[k_offset]>>sum_shift);
|
||||
LOG_I(NR_PHY, "(%i) csi_rs_estimated_determ_fin = %i\n", k, csi_rs_estimated_determ_fin[k_offset]);
|
||||
LOG_I(NR_PHY, "(%i) csi_rs_estimated_numer_fin = %i\n", k, csi_rs_estimated_numer_fin[k_offset]>>sum_shift);
|
||||
#endif
|
||||
|
||||
// compute the conditional number
|
||||
for (int sc_idx=0; sc_idx < NR_NB_SC_PER_RB; sc_idx++) {
|
||||
int8_t csi_rs_estimated_denum_db = dB_fixed(csi_rs_estimated_determ_fin[k_offset + sc_idx]);
|
||||
int8_t csi_rs_estimated_numer_db = dB_fixed(csi_rs_estimated_numer_fin[k_offset + sc_idx]>>sum_shift);
|
||||
int8_t cond_db = csi_rs_estimated_numer_db - csi_rs_estimated_denum_db;
|
||||
for (int sc_idx=0; sc_idx < NR_NB_SC_PER_RB; sc_idx++) {
|
||||
int8_t csi_rs_estimated_denum_db = dB_fixed(csi_rs_estimated_determ_fin[k_offset + sc_idx]);
|
||||
int8_t csi_rs_estimated_numer_db = dB_fixed(csi_rs_estimated_numer_fin[k_offset + sc_idx]>>sum_shift);
|
||||
int8_t cond_db = csi_rs_estimated_numer_db - csi_rs_estimated_denum_db;
|
||||
|
||||
if (cond_db < cond_dB_threshold) {
|
||||
count++;
|
||||
} else {
|
||||
count--;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// ---- General path (N_ports > 2): float Gram matrix + eigenvalue-based rank estimation ----
|
||||
// Compute Gram matrix directly in float from channel estimates to avoid int16 saturation
|
||||
for (int sc_idx = 0; sc_idx < NR_NB_SC_PER_RB; sc_idx++) {
|
||||
float A_re[4][4] = {{0}}, A_im[4][4] = {{0}};
|
||||
// H^H * H: A[p1][p2] = sum_rx conj(H[rx][p1]) * H[rx][p2]
|
||||
for (int rx = 0; rx < frame_parms->nb_antennas_rx; rx++) {
|
||||
for (int p1 = 0; p1 < N_ports; p1++) {
|
||||
c16_t h1 = csi_rs_estimated_channel_freq[rx][p1][k_offset + sc_idx];
|
||||
float h1_re = (float)h1.r;
|
||||
float h1_im = (float)h1.i;
|
||||
for (int p2 = 0; p2 < N_ports; p2++) {
|
||||
c16_t h2 = csi_rs_estimated_channel_freq[rx][p2][k_offset + sc_idx];
|
||||
float h2_re = (float)h2.r;
|
||||
float h2_im = (float)h2.i;
|
||||
// conj(h1) * h2 = (h1_re - j*h1_im) * (h2_re + j*h2_im)
|
||||
A_re[p1][p2] += h1_re * h2_re + h1_im * h2_im;
|
||||
A_im[p1][p2] += -h1_im * h2_re + h1_re * h2_im;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float eig[4] = {0};
|
||||
nr_hermitian_eigenvalues(N_ports, A_re, A_im, eig);
|
||||
|
||||
if (eig[0] < 1e-6f)
|
||||
continue;
|
||||
|
||||
// Determine max supported rank at this subcarrier
|
||||
rank_count[0]++; // at least rank 1
|
||||
for (int r = 1; r < max_rank; r++) {
|
||||
if (eig[r] > 1e-6f && 10.0f * log10f(eig[0] / eig[r]) < eig_ratio_dB_threshold) {
|
||||
rank_count[r]++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#ifdef NR_CSIRS_DEBUG
|
||||
LOG_I(NR_PHY, "csi_rs_estimated_denum_db = %i\n", csi_rs_estimated_denum_db);
|
||||
LOG_I(NR_PHY, "csi_rs_estimated_numer_db = %i\n", csi_rs_estimated_numer_db);
|
||||
LOG_I(NR_PHY, "cond_db = %i\n", cond_db);
|
||||
LOG_I(NR_PHY, "(%i) sc %d eigenvalues: %.1f %.1f %.1f %.1f\n",
|
||||
k, sc_idx, eig[0], eig[1], eig[2], eig[3]);
|
||||
#endif
|
||||
|
||||
if (cond_db < cond_dB_threshold) {
|
||||
count++;
|
||||
} else {
|
||||
count--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// conditional number is lower than cond_dB_threshold in half on more REs
|
||||
if (count > 0) {
|
||||
*rank_indicator = 1;
|
||||
if (N_ports == 2) {
|
||||
// 2x2 decision: existing logic
|
||||
if (count > 0) {
|
||||
*rank_indicator = 1;
|
||||
}
|
||||
} else {
|
||||
// General decision: choose highest rank supported by majority of subcarriers
|
||||
int total_sc = rank_count[0];
|
||||
if (total_sc > 0) {
|
||||
for (int r = max_rank - 1; r >= 1; r--) {
|
||||
if (rank_count[r] > total_sc / 2) {
|
||||
*rank_indicator = r;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef NR_CSIRS_DEBUG
|
||||
LOG_I(NR_PHY, "count = %i\n", count);
|
||||
if (N_ports == 2)
|
||||
LOG_I(NR_PHY, "count = %i\n", count);
|
||||
else
|
||||
LOG_I(NR_PHY, "rank_count = [%i, %i, %i, %i]\n", rank_count[0], rank_count[1], rank_count[2], rank_count[3]);
|
||||
LOG_I(NR_PHY, "rank = %i\n", (*rank_indicator)+1);
|
||||
#endif
|
||||
|
||||
@@ -621,70 +784,381 @@ int nr_csi_rs_pmi_estimation(const PHY_VARS_NR_UE *ue,
|
||||
return 0;
|
||||
}
|
||||
|
||||
if(rank_indicator == 0 || rank_indicator == 1) {
|
||||
c64_t sum[4] = {0};
|
||||
c64_t sum2[4] = {0};
|
||||
int64_t tested_precoded_sinr[4] = {0};
|
||||
if (N_ports == 2) {
|
||||
// ---- 2-port PMI: TS 38.214 Table 5.2.2.2.1-1 ----
|
||||
if(rank_indicator == 0 || rank_indicator == 1) {
|
||||
c64_t sum[4] = {0};
|
||||
c64_t sum2[4] = {0};
|
||||
int64_t tested_precoded_sinr[4] = {0};
|
||||
|
||||
for (int rb = csirs_config_pdu->start_rb; rb < (csirs_config_pdu->start_rb+csirs_config_pdu->nr_of_rbs); rb++) {
|
||||
for (int rb = csirs_config_pdu->start_rb; rb < (csirs_config_pdu->start_rb+csirs_config_pdu->nr_of_rbs); rb++) {
|
||||
|
||||
if (csirs_config_pdu->freq_density <= 1 && csirs_config_pdu->freq_density != (rb % 2)) {
|
||||
continue;
|
||||
}
|
||||
uint16_t k = (frame_parms->first_carrier_offset + rb * NR_NB_SC_PER_RB) % frame_parms->ofdm_symbol_size;
|
||||
uint16_t k_offset = k + mem_offset;
|
||||
for (int ant_rx = 0; ant_rx < frame_parms->nb_antennas_rx; ant_rx++) {
|
||||
const c16_t p0 = csi_rs_estimated_channel_freq[ant_rx][0][k_offset];
|
||||
const c16_t p1 = csi_rs_estimated_channel_freq[ant_rx][1][k_offset];
|
||||
if (csirs_config_pdu->freq_density <= 1 && csirs_config_pdu->freq_density != (rb % 2)) {
|
||||
continue;
|
||||
}
|
||||
uint16_t k = (frame_parms->first_carrier_offset + rb * NR_NB_SC_PER_RB) % frame_parms->ofdm_symbol_size;
|
||||
uint16_t k_offset = k + mem_offset;
|
||||
for (int ant_rx = 0; ant_rx < frame_parms->nb_antennas_rx; ant_rx++) {
|
||||
const c16_t p0 = csi_rs_estimated_channel_freq[ant_rx][0][k_offset];
|
||||
const c16_t p1 = csi_rs_estimated_channel_freq[ant_rx][1][k_offset];
|
||||
|
||||
// H_p0 + 1*H_p1 = (H_p0_re + H_p1_re) + 1j*(H_p0_im + H_p1_im)
|
||||
sum[0].r += (p0.r + p1.r);
|
||||
sum[0].i += (p0.i + p1.i);
|
||||
sum2[0].r += (sum[0].r * sum[0].r) >> log2_re;
|
||||
sum2[0].i += (sum[0].i * sum[0].i) >> log2_re;
|
||||
// H_p0 + 1*H_p1 = (H_p0_re + H_p1_re) + 1j*(H_p0_im + H_p1_im)
|
||||
sum[0].r += (p0.r + p1.r);
|
||||
sum[0].i += (p0.i + p1.i);
|
||||
sum2[0].r += (sum[0].r * sum[0].r) >> log2_re;
|
||||
sum2[0].i += (sum[0].i * sum[0].i) >> log2_re;
|
||||
|
||||
// H_p0 + 1j*H_p1 = (H_p0_re - H_p1_im) + 1j*(H_p0_im + H_p1_re)
|
||||
sum[1].r += (p0.r - p1.i);
|
||||
sum[1].i += (p0.i + p1.r);
|
||||
sum2[1].r += (sum[1].r * sum[1].r) >> log2_re;
|
||||
sum2[1].i += (sum[1].i * sum[1].i) >> log2_re;
|
||||
// H_p0 + 1j*H_p1 = (H_p0_re - H_p1_im) + 1j*(H_p0_im + H_p1_re)
|
||||
sum[1].r += (p0.r - p1.i);
|
||||
sum[1].i += (p0.i + p1.r);
|
||||
sum2[1].r += (sum[1].r * sum[1].r) >> log2_re;
|
||||
sum2[1].i += (sum[1].i * sum[1].i) >> log2_re;
|
||||
|
||||
// H_p0 - 1*H_p1 = (H_p0_re - H_p1_re) + 1j*(H_p0_im - H_p1_im)
|
||||
sum[2].r += (p0.r - p1.r);
|
||||
sum[2].i += (p0.i - p1.i);
|
||||
sum2[2].r += (sum[2].r * sum[2].r) >> log2_re;
|
||||
sum2[2].i += (sum[2].i * sum[2].i) >> log2_re;
|
||||
// H_p0 - 1*H_p1 = (H_p0_re - H_p1_re) + 1j*(H_p0_im - H_p1_im)
|
||||
sum[2].r += (p0.r - p1.r);
|
||||
sum[2].i += (p0.i - p1.i);
|
||||
sum2[2].r += (sum[2].r * sum[2].r) >> log2_re;
|
||||
sum2[2].i += (sum[2].i * sum[2].i) >> log2_re;
|
||||
|
||||
// H_p0 - 1j*H_p1 = (H_p0_re + H_p1_im) + 1j*(H_p0_im - H_p1_re)
|
||||
sum[3].r += (p0.r + p1.i);
|
||||
sum[3].i += (p0.i - p1.r);
|
||||
sum2[3].r += (sum[3].r * sum[3].r) >> log2_re;
|
||||
sum2[3].i += (sum[3].i * sum[3].i) >> log2_re;
|
||||
}
|
||||
}
|
||||
|
||||
// We should perform >>nr_csi_info->log2_re here for all terms, but since sum2_re and sum2_im can be high values,
|
||||
// we performed this above.
|
||||
for(int p = 0; p<4; p++) {
|
||||
int64_t power_re = sum2[p].r - (sum[p].r >> log2_re) * (sum[p].r >> log2_re);
|
||||
int64_t power_im = sum2[p].i - (sum[p].i >> log2_re) * (sum[p].i >> log2_re);
|
||||
tested_precoded_sinr[p] = (power_re + power_im) / interference_plus_noise_power;
|
||||
}
|
||||
|
||||
if(rank_indicator == 0) {
|
||||
for(int tested_i2 = 0; tested_i2 < 4; tested_i2++) {
|
||||
if(tested_precoded_sinr[tested_i2] > tested_precoded_sinr[i2[0]]) {
|
||||
i2[0] = tested_i2;
|
||||
// H_p0 - 1j*H_p1 = (H_p0_re + H_p1_im) + 1j*(H_p0_im - H_p1_re)
|
||||
sum[3].r += (p0.r + p1.i);
|
||||
sum[3].i += (p0.i - p1.r);
|
||||
sum2[3].r += (sum[3].r * sum[3].r) >> log2_re;
|
||||
sum2[3].i += (sum[3].i * sum[3].i) >> log2_re;
|
||||
}
|
||||
}
|
||||
*precoded_sinr_dB = dB_fixed(tested_precoded_sinr[i2[0]]);
|
||||
} else {
|
||||
i2[0] = tested_precoded_sinr[0]+tested_precoded_sinr[2] > tested_precoded_sinr[1]+tested_precoded_sinr[3] ? 0 : 1;
|
||||
*precoded_sinr_dB = dB_fixed((tested_precoded_sinr[i2[0]] + tested_precoded_sinr[i2[0]+2])>>1);
|
||||
}
|
||||
|
||||
for(int p = 0; p<4; p++) {
|
||||
int64_t power_re = sum2[p].r - (sum[p].r >> log2_re) * (sum[p].r >> log2_re);
|
||||
int64_t power_im = sum2[p].i - (sum[p].i >> log2_re) * (sum[p].i >> log2_re);
|
||||
tested_precoded_sinr[p] = (power_re + power_im) / interference_plus_noise_power;
|
||||
}
|
||||
|
||||
if(rank_indicator == 0) {
|
||||
for(int tested_i2 = 0; tested_i2 < 4; tested_i2++) {
|
||||
if(tested_precoded_sinr[tested_i2] > tested_precoded_sinr[i2[0]]) {
|
||||
i2[0] = tested_i2;
|
||||
}
|
||||
}
|
||||
*precoded_sinr_dB = dB_fixed(tested_precoded_sinr[i2[0]]);
|
||||
} else {
|
||||
i2[0] = tested_precoded_sinr[0]+tested_precoded_sinr[2] > tested_precoded_sinr[1]+tested_precoded_sinr[3] ? 0 : 1;
|
||||
*precoded_sinr_dB = dB_fixed((tested_precoded_sinr[i2[0]] + tested_precoded_sinr[i2[0]+2])>>1);
|
||||
}
|
||||
} else {
|
||||
LOG_W(NR_PHY, "PMI computation is not implemented for rank indicator %i with 2 ports\n", rank_indicator+1);
|
||||
return -1;
|
||||
}
|
||||
} else if (N_ports == 4) {
|
||||
// ---- 4-port PMI: Type1SinglePanel codebook, TS 38.214 Table 5.2.2.2.1-5 ----
|
||||
// N1=2, N2=1, O1=4, O2=1
|
||||
// Rank 1 precoder: W(l,n) = 1/2 * [1; w1_l; w2_n; w1_l*w2_n]^T
|
||||
// where w1_l = e^{j*pi*l/4} (l = i11 in {0,...,7}), w2_n = e^{j*pi*n/2} (n = i2 in {0,1,2,3})
|
||||
// Rank 2 precoder: W(l,n) = 1/(2*sqrt(2)) * [v_l v_l; w2_n*v_l -w2_n*v_l]
|
||||
// where v_l = [1; w1_l]^T, n = i2 in {0,1}
|
||||
|
||||
// Precompute DFT beam phases w1_l = e^{j*pi*l/4} for l=0..7
|
||||
// Stored as (cos, sin) pairs scaled to fixed-point is not needed; we use float for the search.
|
||||
const int N_beams = 8; // N1 * O1 = 2 * 4
|
||||
float w1_re[8], w1_im[8];
|
||||
for (int l = 0; l < N_beams; l++) {
|
||||
w1_re[l] = cosf((float)M_PI * l / 4.0f);
|
||||
w1_im[l] = sinf((float)M_PI * l / 4.0f);
|
||||
}
|
||||
// Co-phasing factors w2_n = e^{j*pi*n/2} for n=0..3
|
||||
const float w2_re[4] = {1.0f, 0.0f, -1.0f, 0.0f};
|
||||
const float w2_im[4] = {0.0f, 1.0f, 0.0f, -1.0f};
|
||||
|
||||
if (rank_indicator == 0) {
|
||||
// Rank 1: search over i11 (beam l) and i2 (co-phase n)
|
||||
// Effective channel: h_eff = H[rx][0] + w1*H[rx][1] + w2*H[rx][2] + w1*w2*H[rx][3]
|
||||
// = (H[rx][0] + w1*H[rx][1]) + w2*(H[rx][2] + w1*H[rx][3])
|
||||
int64_t best_sinr = -1;
|
||||
uint8_t best_l = 0, best_n = 0;
|
||||
|
||||
for (int l = 0; l < N_beams; l++) {
|
||||
// For this beam l, accumulate effective channel power for each co-phase n
|
||||
int64_t power[4] = {0};
|
||||
int rb_count = 0;
|
||||
|
||||
for (int rb = csirs_config_pdu->start_rb; rb < (csirs_config_pdu->start_rb + csirs_config_pdu->nr_of_rbs); rb++) {
|
||||
if (csirs_config_pdu->freq_density <= 1 && csirs_config_pdu->freq_density != (rb % 2))
|
||||
continue;
|
||||
uint16_t k = (frame_parms->first_carrier_offset + rb * NR_NB_SC_PER_RB) % frame_parms->ofdm_symbol_size;
|
||||
uint16_t k_offset = k + mem_offset;
|
||||
rb_count++;
|
||||
|
||||
for (int ant_rx = 0; ant_rx < frame_parms->nb_antennas_rx; ant_rx++) {
|
||||
const c16_t h0 = csi_rs_estimated_channel_freq[ant_rx][0][k_offset];
|
||||
const c16_t h1 = csi_rs_estimated_channel_freq[ant_rx][1][k_offset];
|
||||
const c16_t h2 = csi_rs_estimated_channel_freq[ant_rx][2][k_offset];
|
||||
const c16_t h3 = csi_rs_estimated_channel_freq[ant_rx][3][k_offset];
|
||||
|
||||
// A = h0 + w1*h1 (complex multiply)
|
||||
float A_re = (float)h0.r + w1_re[l] * h1.r - w1_im[l] * h1.i;
|
||||
float A_im = (float)h0.i + w1_re[l] * h1.i + w1_im[l] * h1.r;
|
||||
// B = h2 + w1*h3
|
||||
float B_re = (float)h2.r + w1_re[l] * h3.r - w1_im[l] * h3.i;
|
||||
float B_im = (float)h2.i + w1_re[l] * h3.i + w1_im[l] * h3.r;
|
||||
|
||||
// h_eff(n) = A + w2_n * B for each co-phase n
|
||||
for (int n = 0; n < 4; n++) {
|
||||
float eff_re = A_re + w2_re[n] * B_re - w2_im[n] * B_im;
|
||||
float eff_im = A_im + w2_re[n] * B_im + w2_im[n] * B_re;
|
||||
power[n] += (int64_t)(eff_re * eff_re + eff_im * eff_im);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Select best co-phase for this beam
|
||||
for (int n = 0; n < 4; n++) {
|
||||
if (power[n] > best_sinr) {
|
||||
best_sinr = power[n];
|
||||
best_l = l;
|
||||
best_n = n;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i1[0] = best_l; // i11
|
||||
i1[1] = 0; // i12 (not applicable for N2=1)
|
||||
i1[2] = 0; // i13 (not applicable for rank 1)
|
||||
i2[0] = best_n;
|
||||
if (best_sinr > 0)
|
||||
*precoded_sinr_dB = dB_fixed(best_sinr / interference_plus_noise_power);
|
||||
|
||||
} else if (rank_indicator == 1) {
|
||||
// Rank 2: two-layer precoder
|
||||
// W(l,n) = 1/(2*sqrt(2)) * [v_l v_l; phi_n*v_l -phi_n*v_l]
|
||||
// Column 0: [1; w1_l; w2_n; w1_l*w2_n]^T (same as rank 1)
|
||||
// Column 1: [1; w1_l; -w2_n; -w1_l*w2_n]^T
|
||||
// SINR ~ sum of per-layer powers, co-phase n in {0,1}
|
||||
int64_t best_sinr = -1;
|
||||
uint8_t best_l = 0, best_n = 0;
|
||||
|
||||
for (int l = 0; l < N_beams; l++) {
|
||||
int64_t power[2] = {0}; // 2 co-phase hypotheses for rank 2
|
||||
|
||||
for (int rb = csirs_config_pdu->start_rb; rb < (csirs_config_pdu->start_rb + csirs_config_pdu->nr_of_rbs); rb++) {
|
||||
if (csirs_config_pdu->freq_density <= 1 && csirs_config_pdu->freq_density != (rb % 2))
|
||||
continue;
|
||||
uint16_t k = (frame_parms->first_carrier_offset + rb * NR_NB_SC_PER_RB) % frame_parms->ofdm_symbol_size;
|
||||
uint16_t k_offset = k + mem_offset;
|
||||
|
||||
for (int ant_rx = 0; ant_rx < frame_parms->nb_antennas_rx; ant_rx++) {
|
||||
const c16_t h0 = csi_rs_estimated_channel_freq[ant_rx][0][k_offset];
|
||||
const c16_t h1 = csi_rs_estimated_channel_freq[ant_rx][1][k_offset];
|
||||
const c16_t h2 = csi_rs_estimated_channel_freq[ant_rx][2][k_offset];
|
||||
const c16_t h3 = csi_rs_estimated_channel_freq[ant_rx][3][k_offset];
|
||||
|
||||
float A_re = (float)h0.r + w1_re[l] * h1.r - w1_im[l] * h1.i;
|
||||
float A_im = (float)h0.i + w1_re[l] * h1.i + w1_im[l] * h1.r;
|
||||
float B_re = (float)h2.r + w1_re[l] * h3.r - w1_im[l] * h3.i;
|
||||
float B_im = (float)h2.i + w1_re[l] * h3.i + w1_im[l] * h3.r;
|
||||
|
||||
for (int n = 0; n < 2; n++) {
|
||||
// Layer 0: A + w2_n * B
|
||||
float l0_re = A_re + w2_re[n] * B_re - w2_im[n] * B_im;
|
||||
float l0_im = A_im + w2_re[n] * B_im + w2_im[n] * B_re;
|
||||
// Layer 1: A - w2_n * B
|
||||
float l1_re = A_re - w2_re[n] * B_re + w2_im[n] * B_im;
|
||||
float l1_im = A_im - w2_re[n] * B_im - w2_im[n] * B_re;
|
||||
power[n] += (int64_t)(l0_re * l0_re + l0_im * l0_im + l1_re * l1_re + l1_im * l1_im);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int n = 0; n < 2; n++) {
|
||||
if (power[n] > best_sinr) {
|
||||
best_sinr = power[n];
|
||||
best_l = l;
|
||||
best_n = n;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i1[0] = best_l;
|
||||
i1[1] = 0;
|
||||
i1[2] = 0;
|
||||
i2[0] = best_n;
|
||||
if (best_sinr > 0)
|
||||
*precoded_sinr_dB = dB_fixed((best_sinr / 2) / interference_plus_noise_power);
|
||||
|
||||
} else if (rank_indicator == 2) {
|
||||
// Rank 3: three-layer precoder using beam pairs (TS 38.214 Table 5.2.2.2.1-5)
|
||||
// W(l,n) = 1/(2*sqrt(3)) * [v_a v_a v_b; phi_n*v_a -phi_n*v_a phi_n*v_b]
|
||||
// where v_a = [1; w1_{2l}], v_b = [1; -w1_{2l}] (orthogonal beam pair)
|
||||
// i1[0] = l in {0,...,N1*O1/2-1} = {0,1,2,3}, i2[0] = n in {0,1}
|
||||
// Layer 0: A + phi*B, Layer 1: A - phi*B, Layer 2: A' + phi*B'
|
||||
// where A = h0 + w1*h1, B = h2 + w1*h3, A' = h0 - w1*h1, B' = h2 - w1*h3
|
||||
int64_t best_sinr = -1;
|
||||
uint8_t best_l = 0, best_n = 0;
|
||||
const int N_beam_pairs = 4; // N1*O1/2
|
||||
|
||||
for (int l = 0; l < N_beam_pairs; l++) {
|
||||
int beam_a = 2 * l; // first beam index in DFT grid
|
||||
int64_t power[2] = {0};
|
||||
|
||||
for (int rb = csirs_config_pdu->start_rb; rb < (csirs_config_pdu->start_rb + csirs_config_pdu->nr_of_rbs); rb++) {
|
||||
if (csirs_config_pdu->freq_density <= 1 && csirs_config_pdu->freq_density != (rb % 2))
|
||||
continue;
|
||||
uint16_t k = (frame_parms->first_carrier_offset + rb * NR_NB_SC_PER_RB) % frame_parms->ofdm_symbol_size;
|
||||
uint16_t k_offset = k + mem_offset;
|
||||
|
||||
for (int ant_rx = 0; ant_rx < frame_parms->nb_antennas_rx; ant_rx++) {
|
||||
const c16_t h0 = csi_rs_estimated_channel_freq[ant_rx][0][k_offset];
|
||||
const c16_t h1 = csi_rs_estimated_channel_freq[ant_rx][1][k_offset];
|
||||
const c16_t h2 = csi_rs_estimated_channel_freq[ant_rx][2][k_offset];
|
||||
const c16_t h3 = csi_rs_estimated_channel_freq[ant_rx][3][k_offset];
|
||||
|
||||
float w1r = w1_re[beam_a], w1i = w1_im[beam_a];
|
||||
|
||||
// A = h0 + w1*h1 (beam a, ports 0-1)
|
||||
float A_re = (float)h0.r + w1r * h1.r - w1i * h1.i;
|
||||
float A_im = (float)h0.i + w1r * h1.i + w1i * h1.r;
|
||||
// B = h2 + w1*h3 (beam a, ports 2-3)
|
||||
float B_re = (float)h2.r + w1r * h3.r - w1i * h3.i;
|
||||
float B_im = (float)h2.i + w1r * h3.i + w1i * h3.r;
|
||||
// A' = h0 - w1*h1 (beam b, ports 0-1)
|
||||
float Ap_re = (float)h0.r - w1r * h1.r + w1i * h1.i;
|
||||
float Ap_im = (float)h0.i - w1r * h1.i - w1i * h1.r;
|
||||
// B' = h2 - w1*h3 (beam b, ports 2-3)
|
||||
float Bp_re = (float)h2.r - w1r * h3.r + w1i * h3.i;
|
||||
float Bp_im = (float)h2.i - w1r * h3.i - w1i * h3.r;
|
||||
|
||||
for (int n = 0; n < 2; n++) {
|
||||
// Layer 0: A + phi_n * B
|
||||
float l0_re = A_re + w2_re[n] * B_re - w2_im[n] * B_im;
|
||||
float l0_im = A_im + w2_re[n] * B_im + w2_im[n] * B_re;
|
||||
// Layer 1: A - phi_n * B
|
||||
float l1_re = A_re - w2_re[n] * B_re + w2_im[n] * B_im;
|
||||
float l1_im = A_im - w2_re[n] * B_im - w2_im[n] * B_re;
|
||||
// Layer 2: A' + phi_n * B'
|
||||
float l2_re = Ap_re + w2_re[n] * Bp_re - w2_im[n] * Bp_im;
|
||||
float l2_im = Ap_im + w2_re[n] * Bp_im + w2_im[n] * Bp_re;
|
||||
|
||||
power[n] += (int64_t)(l0_re * l0_re + l0_im * l0_im
|
||||
+ l1_re * l1_re + l1_im * l1_im
|
||||
+ l2_re * l2_re + l2_im * l2_im);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int n = 0; n < 2; n++) {
|
||||
if (power[n] > best_sinr) {
|
||||
best_sinr = power[n];
|
||||
best_l = l;
|
||||
best_n = n;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i1[0] = best_l;
|
||||
i1[1] = 0;
|
||||
i1[2] = 0;
|
||||
i2[0] = best_n;
|
||||
if (best_sinr > 0)
|
||||
*precoded_sinr_dB = dB_fixed((best_sinr / 3) / interference_plus_noise_power);
|
||||
|
||||
} else if (rank_indicator == 3) {
|
||||
// Rank 4: four-layer precoder using beam pairs (TS 38.214 Table 5.2.2.2.1-5)
|
||||
// W(l,n) = 1/4 * [v_a v_a v_b v_b; phi_n*v_a -phi_n*v_a phi_n*v_b -phi_n*v_b]
|
||||
// Layer 0: A + phi*B, Layer 1: A - phi*B, Layer 2: A' + phi*B', Layer 3: A' - phi*B'
|
||||
int64_t best_sinr = -1;
|
||||
uint8_t best_l = 0, best_n = 0;
|
||||
const int N_beam_pairs = 4;
|
||||
|
||||
for (int l = 0; l < N_beam_pairs; l++) {
|
||||
int beam_a = 2 * l;
|
||||
int64_t power[2] = {0};
|
||||
|
||||
for (int rb = csirs_config_pdu->start_rb; rb < (csirs_config_pdu->start_rb + csirs_config_pdu->nr_of_rbs); rb++) {
|
||||
if (csirs_config_pdu->freq_density <= 1 && csirs_config_pdu->freq_density != (rb % 2))
|
||||
continue;
|
||||
uint16_t k = (frame_parms->first_carrier_offset + rb * NR_NB_SC_PER_RB) % frame_parms->ofdm_symbol_size;
|
||||
uint16_t k_offset = k + mem_offset;
|
||||
|
||||
for (int ant_rx = 0; ant_rx < frame_parms->nb_antennas_rx; ant_rx++) {
|
||||
const c16_t h0 = csi_rs_estimated_channel_freq[ant_rx][0][k_offset];
|
||||
const c16_t h1 = csi_rs_estimated_channel_freq[ant_rx][1][k_offset];
|
||||
const c16_t h2 = csi_rs_estimated_channel_freq[ant_rx][2][k_offset];
|
||||
const c16_t h3 = csi_rs_estimated_channel_freq[ant_rx][3][k_offset];
|
||||
|
||||
float w1r = w1_re[beam_a], w1i = w1_im[beam_a];
|
||||
|
||||
float A_re = (float)h0.r + w1r * h1.r - w1i * h1.i;
|
||||
float A_im = (float)h0.i + w1r * h1.i + w1i * h1.r;
|
||||
float B_re = (float)h2.r + w1r * h3.r - w1i * h3.i;
|
||||
float B_im = (float)h2.i + w1r * h3.i + w1i * h3.r;
|
||||
float Ap_re = (float)h0.r - w1r * h1.r + w1i * h1.i;
|
||||
float Ap_im = (float)h0.i - w1r * h1.i - w1i * h1.r;
|
||||
float Bp_re = (float)h2.r - w1r * h3.r + w1i * h3.i;
|
||||
float Bp_im = (float)h2.i - w1r * h3.i - w1i * h3.r;
|
||||
|
||||
for (int n = 0; n < 2; n++) {
|
||||
float l0_re = A_re + w2_re[n] * B_re - w2_im[n] * B_im;
|
||||
float l0_im = A_im + w2_re[n] * B_im + w2_im[n] * B_re;
|
||||
float l1_re = A_re - w2_re[n] * B_re + w2_im[n] * B_im;
|
||||
float l1_im = A_im - w2_re[n] * B_im - w2_im[n] * B_re;
|
||||
float l2_re = Ap_re + w2_re[n] * Bp_re - w2_im[n] * Bp_im;
|
||||
float l2_im = Ap_im + w2_re[n] * Bp_im + w2_im[n] * Bp_re;
|
||||
float l3_re = Ap_re - w2_re[n] * Bp_re + w2_im[n] * Bp_im;
|
||||
float l3_im = Ap_im - w2_re[n] * Bp_im - w2_im[n] * Bp_re;
|
||||
|
||||
power[n] += (int64_t)(l0_re * l0_re + l0_im * l0_im
|
||||
+ l1_re * l1_re + l1_im * l1_im
|
||||
+ l2_re * l2_re + l2_im * l2_im
|
||||
+ l3_re * l3_re + l3_im * l3_im);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int n = 0; n < 2; n++) {
|
||||
if (power[n] > best_sinr) {
|
||||
best_sinr = power[n];
|
||||
best_l = l;
|
||||
best_n = n;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i1[0] = best_l;
|
||||
i1[1] = 0;
|
||||
i1[2] = 0;
|
||||
i2[0] = best_n;
|
||||
if (best_sinr > 0)
|
||||
*precoded_sinr_dB = dB_fixed((best_sinr / 4) / interference_plus_noise_power);
|
||||
|
||||
} else {
|
||||
// RI=3 or RI=4: Use identity-like precoder (i1=0, i2=0)
|
||||
// Compute SINR from per-layer effective channel power with identity precoding
|
||||
// Each layer p sees channel H[:,p] across all RX antennas
|
||||
int nlayers = rank_indicator + 1;
|
||||
int64_t total_power = 0;
|
||||
|
||||
for (int rb = csirs_config_pdu->start_rb; rb < (csirs_config_pdu->start_rb + csirs_config_pdu->nr_of_rbs); rb++) {
|
||||
if (csirs_config_pdu->freq_density <= 1 && csirs_config_pdu->freq_density != (rb % 2))
|
||||
continue;
|
||||
uint16_t k = (frame_parms->first_carrier_offset + rb * NR_NB_SC_PER_RB) % frame_parms->ofdm_symbol_size;
|
||||
uint16_t k_off = k + mem_offset;
|
||||
|
||||
for (int ant_rx = 0; ant_rx < frame_parms->nb_antennas_rx; ant_rx++) {
|
||||
for (int p = 0; p < nlayers; p++) {
|
||||
const c16_t h = csi_rs_estimated_channel_freq[ant_rx][p][k_off];
|
||||
total_power += (int64_t)h.r * h.r + (int64_t)h.i * h.i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i1[0] = 0;
|
||||
i1[1] = 0;
|
||||
i1[2] = 0;
|
||||
i2[0] = 0;
|
||||
if (total_power > 0)
|
||||
*precoded_sinr_dB = dB_fixed((total_power / nlayers) / interference_plus_noise_power);
|
||||
}
|
||||
} else {
|
||||
LOG_W(NR_PHY, "PMI computation is not implemented for rank indicator %i\n", rank_indicator+1);
|
||||
LOG_W(NR_PHY, "PMI computation is not implemented for %i ports\n", N_ports);
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
#include "PHY/CODING/nrLDPC_extern.h"
|
||||
#include "PHY/CODING/nrLDPC_decoder/nrLDPC_types.h"
|
||||
#include "nfapi_nr_interface_scf.h"
|
||||
#include "PHY/NR_TRANSPORT/nr_antenna_state.h"
|
||||
|
||||
#define MAX_NUM_RU_PER_gNB 8
|
||||
#define MAX_PUCCH0_NID 8
|
||||
@@ -445,6 +446,8 @@ typedef struct PHY_VARS_gNB_s {
|
||||
|
||||
bool enable_analog_das;
|
||||
|
||||
nr_antenna_state_t antenna_state;
|
||||
|
||||
time_stats_t l1_tx_proc;
|
||||
time_stats_t l1_rx_proc;
|
||||
|
||||
|
||||
@@ -46,6 +46,14 @@
|
||||
|
||||
#include <time.h>
|
||||
|
||||
static inline int count_nnz_c16(const c16_t *v, int n)
|
||||
{
|
||||
int nnz = 0;
|
||||
for (int i = 0; i < n; i++)
|
||||
nnz += (v[i].r != 0) || (v[i].i != 0);
|
||||
return nnz;
|
||||
}
|
||||
|
||||
// RU OFDM Modulator gNodeB
|
||||
// OFDM modulation core routine, generates a first_symbol to first_symbol+num_symbols on a particular slot and TX antenna port
|
||||
void nr_feptx0(RU_t *ru, int tti_tx, int first_symbol, int num_symbols, int aa)
|
||||
@@ -203,12 +211,21 @@ void nr_feptx_prec(RU_t *ru, int frame_tx, int slot_tx)
|
||||
int Ptx = cfg->carrier_config.num_tx_ant.value;
|
||||
// If there is no digital beamforming we just need to copy the data to RU
|
||||
if (ru->config.dbt_config.num_dig_beams == 0 || ru->gNB_list[0]->common_vars.analog_bf) {
|
||||
const nr_antenna_state_t *ant_state = &gNB->antenna_state;
|
||||
for (int b = 0; b < ru->num_beams_period; b++) {
|
||||
for (int i = 0; i < Ptx; ++i) {
|
||||
int tx_idx = i + b * ru->nb_tx;
|
||||
memcpy((void*)ru->common.txdataF_BF[tx_idx],
|
||||
(void*)&gNB->common_vars.txdataF[b][i][txdataF_offset],
|
||||
fp->samples_per_slot_wCP * sizeof(int32_t));
|
||||
// Check antenna muting state for energy saving
|
||||
if (!nr_is_tx_antenna_active(ant_state, i)) {
|
||||
// Antenna is muted - zero out the buffer (transmission blanking)
|
||||
memset((void*)ru->common.txdataF_BF[tx_idx], 0, fp->samples_per_slot_wCP * sizeof(int32_t));
|
||||
LOG_D(PHY, "[ANTENNA_MUTE] TX antenna %d muted in nr_feptx_prec, blanking slot %d\n", i, slot_tx);
|
||||
} else {
|
||||
// Antenna is active - copy data normally
|
||||
memcpy((void*)ru->common.txdataF_BF[tx_idx],
|
||||
(void*)&gNB->common_vars.txdataF[b][i][txdataF_offset],
|
||||
fp->samples_per_slot_wCP * sizeof(int32_t));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -246,14 +263,40 @@ void nr_feptx(void *arg)
|
||||
}
|
||||
|
||||
// If there is no digital beamforming we just need to copy the data to RU
|
||||
if (ru->config.dbt_config.num_dig_beams == 0 || ru->gNB_list[0]->common_vars.analog_bf)
|
||||
memcpy((void*)&ru->common.txdataF_BF[tx_idx][txdataF_BF_offset],
|
||||
(void*)&ru->gNB_list[0]->common_vars.txdataF[bb][aa][txdataF_offset],
|
||||
numSamples * sizeof(int32_t));
|
||||
if (ru->config.dbt_config.num_dig_beams == 0 || ru->gNB_list[0]->common_vars.analog_bf) {
|
||||
// Check antenna muting state for energy saving
|
||||
const nr_antenna_state_t *ant_state = &ru->gNB_list[0]->antenna_state;
|
||||
bool is_active = nr_is_tx_antenna_active(ant_state, aa);
|
||||
if (!is_active) {
|
||||
// Antenna is muted - zero out the buffer (transmission blanking)
|
||||
memset((void*)&ru->common.txdataF_BF[tx_idx][txdataF_BF_offset], 0, numSamples * sizeof(int32_t));
|
||||
/* if (startSymbol == 0) { */
|
||||
/* LOG_I(PHY, "[ANTENNA_MUTE] TX ant %d muted (mask=0x%02X, num_tx=%d), blanking slot %d\n", */
|
||||
/* aa, nr_get_tx_antenna_bitmask(ant_state), ant_state->num_tx_antennas, slot); */
|
||||
/* } */
|
||||
} else {
|
||||
// Antenna is active - copy data normally
|
||||
memcpy((void*)&ru->common.txdataF_BF[tx_idx][txdataF_BF_offset],
|
||||
(void*)&ru->gNB_list[0]->common_vars.txdataF[bb][aa][txdataF_offset],
|
||||
numSamples * sizeof(int32_t));
|
||||
}
|
||||
}
|
||||
else {
|
||||
AssertFatal(false, "This needs to be fixed by using appropriate beams from config\n");
|
||||
AssertFatal(false, "This needs to be fixed by using appropriate beams from config\n");
|
||||
}
|
||||
|
||||
/* const c16_t *dst = (const c16_t *)&ru->common.txdataF_BF[tx_idx][txdataF_BF_offset]; */
|
||||
/* const int dst_nnz = count_nnz_c16(dst, numSamples); */
|
||||
/* if (startSymbol == 0 && ru->proc.frame_tx % 10 == 0 && dst_nnz != 0) { */
|
||||
/* const int dst_pwr_db = dB_fixed(signal_energy_nodc((c16_t *)dst, 2 * numSamples)); */
|
||||
|
||||
/* LOG_I(PHY, */
|
||||
/* "[TXF_PWR] frame=%d slot=%d beam=%d aa=%d tx_idx=%d " */
|
||||
/* "DST pwr=%d dB nnz=%d\n", */
|
||||
/* ru->proc.frame_tx, slot, bb, aa, tx_idx, dst_pwr_db, dst_nnz); */
|
||||
/* } */
|
||||
|
||||
|
||||
if (tx_idx == 0)
|
||||
stop_meas(&ru->precoding_stats);
|
||||
VCD_SIGNAL_DUMPER_DUMP_FUNCTION_BY_NAME(VCD_SIGNAL_DUMPER_FUNCTIONS_PHY_PROCEDURES_RU_FEPTX_PREC+feptx->aid , 0);
|
||||
|
||||
@@ -108,7 +108,10 @@ bool read_pdcp_sm(void* data)
|
||||
}
|
||||
num_ues++;
|
||||
|
||||
AssertFatal(num_ues < MAX_MOBILES_PER_GNB, "cannot have more UEs than global UE number maximum\n");
|
||||
if (num_ues >= MAX_MOBILES_PER_GNB) {
|
||||
LOG_W(PDCP, "num_ues %zu >= MAX_MOBILES_PER_GNB %d, skipping remaining UEs\n", num_ues, MAX_MOBILES_PER_GNB);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#elif defined (NGRAN_GNB_CUUP)
|
||||
|
||||
@@ -178,7 +178,7 @@ static void capture_sst_sd(test_cond_value_t* test_cond_value, uint8_t *sst, uin
|
||||
*sd = malloc(sizeof(uint32_t));
|
||||
**sd = buf[1] << 16 | buf[2] << 8 | buf[3];
|
||||
}
|
||||
printf("[E2 AGENT][E2SM-KPM] Condition NSSAI (%d, %x).\n", *sst, (*sd) ? **sd : 0xffffff);
|
||||
/* printf("[E2 AGENT][E2SM-KPM] Condition NSSAI (sst=%d, sd=%x).\n", *sst, (*sd) ? **sd : 0xffffff); */
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -226,7 +226,10 @@ static arr_ue_id_t filter_ues_by_s_nssai_in_cu(const test_info_lst_t test_info)
|
||||
break;
|
||||
}
|
||||
}
|
||||
AssertFatal(arr_ue_id.sz < MAX_MOBILES_PER_GNB, "cannot have more UEs than global UE number maximum\n");
|
||||
if (arr_ue_id.sz >= MAX_MOBILES_PER_GNB) {
|
||||
LOG_W(NR_MAC, "arr_ue_id.sz %zu >= MAX_MOBILES_PER_GNB %d, skipping remaining UEs\n", arr_ue_id.sz, MAX_MOBILES_PER_GNB);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
free(sd); // if NULL, nothing happens
|
||||
@@ -319,7 +322,10 @@ static arr_ue_id_t filter_ues_by_s_nssai_in_du_or_monolithic(const test_info_lst
|
||||
break;
|
||||
}
|
||||
}
|
||||
AssertFatal(arr_ue_id.sz < MAX_MOBILES_PER_GNB, "cannot have more UEs than global UE number maximum\n");
|
||||
if (arr_ue_id.sz >= MAX_MOBILES_PER_GNB) {
|
||||
LOG_W(NR_MAC, "arr_ue_id.sz %zu >= MAX_MOBILES_PER_GNB %d, skipping remaining UEs\n", arr_ue_id.sz, MAX_MOBILES_PER_GNB);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
node_stats[1].mac_stats.dl.total_prb_aggregate = mac->mac_stats.dl.total_prb_aggregate;
|
||||
|
||||
+1
-1
Submodule openair2/E2AP/flexric updated: 6a595d8b24...205df5ae48
@@ -435,7 +435,7 @@ static bool allocate_dl_retransmission(gNB_MAC_INST *nr_mac,
|
||||
NR_UE_DL_BWP_t *dl_bwp = &UE->current_DL_BWP;
|
||||
NR_UE_UL_BWP_t *ul_bwp = &UE->current_UL_BWP;
|
||||
NR_sched_pdsch_t new_sched = sched_ctrl->harq_processes[current_harq_pid].sched_pdsch;
|
||||
int layers = get_dl_nrOfLayers(sched_ctrl, dl_bwp->dci_format);
|
||||
int layers = get_dl_nrOfLayers(nr_mac, sched_ctrl, dl_bwp->dci_format);
|
||||
int pm_index = get_pm_index(nr_mac, UE, dl_bwp->dci_format, layers, nr_mac->radio_config.pdsch_AntennaPorts.XP);
|
||||
|
||||
// If the RI changed between current rtx and a previous transmission
|
||||
@@ -597,7 +597,6 @@ static bool allocate_dl_retransmission(gNB_MAC_INST *nr_mac,
|
||||
static void ack_reconfig(gNB_MAC_INST *mac, NR_UE_info_t *UE)
|
||||
{
|
||||
if (!UE->reconfigCellGroup) {
|
||||
LOG_W(NR_MAC, "Received ACK for RRCReconfiguration, but nothing to apply!\n");
|
||||
return;
|
||||
}
|
||||
ASN_STRUCT_FREE(asn_DEF_NR_CellGroupConfig, UE->CellGroup);
|
||||
@@ -732,7 +731,7 @@ static void pf_dl(gNB_MAC_INST *mac,
|
||||
sched_ctrl->dl_bler_stats.mcs = selected_mcs;
|
||||
} else
|
||||
selected_mcs = get_mcs_from_bler(bo, stats, &sched_ctrl->dl_bler_stats, max_mcs, frame);
|
||||
int l = get_dl_nrOfLayers(sched_ctrl, current_BWP->dci_format);
|
||||
int l = get_dl_nrOfLayers(mac, sched_ctrl, current_BWP->dci_format);
|
||||
const uint8_t Qm = nr_get_Qm_dl(selected_mcs, current_BWP->mcsTableIdx);
|
||||
const uint16_t R = nr_get_code_rate_dl(selected_mcs, current_BWP->mcsTableIdx);
|
||||
uint32_t tbs = nr_compute_tbs(Qm,
|
||||
@@ -877,7 +876,7 @@ static void pf_dl(gNB_MAC_INST *mac,
|
||||
sched_ctrl->cce_index = CCEIndex;
|
||||
fill_pdcch_vrb_map(mac, CC_id, &sched_ctrl->sched_pdcch, CCEIndex, sched_ctrl->aggregation_level, beam.idx);
|
||||
|
||||
int l = get_dl_nrOfLayers(sched_ctrl, dl_bwp->dci_format);
|
||||
int l = get_dl_nrOfLayers(mac, sched_ctrl, dl_bwp->dci_format);
|
||||
NR_sched_pdsch_t sched_pdsch = {
|
||||
// rbSize below
|
||||
.rbStart = rbStart,
|
||||
|
||||
@@ -127,14 +127,13 @@ static void determine_aggregation_level_search_order(int agg_level_search_order[
|
||||
|
||||
static int nr_mac_interrupt_ue_transmission(gNB_MAC_INST *mac, NR_UE_info_t *UE, int slots, int slots_per_frame);
|
||||
|
||||
uint8_t get_dl_nrOfLayers(const NR_UE_sched_ctrl_t *sched_ctrl, const nr_dci_format_t dci_format)
|
||||
uint8_t get_dl_nrOfLayers(const gNB_MAC_INST *mac, const NR_UE_sched_ctrl_t *sched_ctrl, const nr_dci_format_t dci_format)
|
||||
{
|
||||
// TODO check this but it should be enough for now
|
||||
// if there is not csi report activated RI is 0 from initialization
|
||||
if(dci_format == NR_DL_DCI_FORMAT_1_0)
|
||||
return 1;
|
||||
else
|
||||
return sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.ri + 1;
|
||||
int layers = (dci_format == NR_DL_DCI_FORMAT_1_0) ? 1
|
||||
: sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.ri + 1;
|
||||
if (mac && mac->antenna_state)
|
||||
return nr_get_effective_mimo_layers(mac->antenna_state, layers);
|
||||
return layers;
|
||||
}
|
||||
|
||||
int get_ul_nrOfLayers(const NR_UE_sched_ctrl_t *sched_ctrl, const nr_dci_format_t dci_format)
|
||||
@@ -191,63 +190,88 @@ uint16_t get_pm_index(const gNB_MAC_INST *nrmac,
|
||||
int layers,
|
||||
int xp_pdsch_antenna_ports)
|
||||
{
|
||||
if (dci_format == NR_DL_DCI_FORMAT_1_0 || nrmac->identity_pm || xp_pdsch_antenna_ports == 1)
|
||||
return 0; //identity matrix (basic 5G configuration handled by PMI report is with XP antennas)
|
||||
const NR_UE_sched_ctrl_t *sched_ctrl = &UE->UE_sched_ctrl;
|
||||
const int report_id = sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.csi_report_id;
|
||||
const nr_csi_report_t *csi_report = &UE->csi_report_template[report_id];
|
||||
const int N1 = csi_report->N1;
|
||||
const int N2 = csi_report->N2;
|
||||
const int antenna_ports = (N1 * N2) << 1;
|
||||
if (antenna_ports < 2)
|
||||
return 0; // single antenna port
|
||||
if (dci_format == NR_DL_DCI_FORMAT_1_0 || nrmac->identity_pm || xp_pdsch_antenna_ports == 1)
|
||||
return 0; //identity matrix (basic 5G configuration handled by PMI report is with XP antennas)
|
||||
const NR_UE_sched_ctrl_t *sched_ctrl = &UE->UE_sched_ctrl;
|
||||
|
||||
int x1 = sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.pmi_x1;
|
||||
const int x2 = sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.pmi_x2;
|
||||
LOG_D(NR_MAC,"PMI report: x1 %d x2 %d layers: %d\n", x1, x2, layers);
|
||||
|
||||
int prev_layers_size = 0;
|
||||
for (int i = 1; i < layers; i++)
|
||||
prev_layers_size += nrmac->precoding_matrix_size[i - 1];
|
||||
|
||||
// need to return PM index to matrix initialized in init_DL_MIMO_codebook
|
||||
// index 0 is for identity matrix
|
||||
// order of matrices depends on layers to be transmitted
|
||||
// elements from 1 to n for 1 layer
|
||||
// elements from n+1 to m for 2 layers etc.
|
||||
if (antenna_ports == 2)
|
||||
return 1 + prev_layers_size + x2; // 0 for identity matrix
|
||||
else {
|
||||
int idx = layers - 1;
|
||||
// the order of i1x in X1 report needs to be verified
|
||||
// the standard is not very clear (Table 6.3.1.1.2-7 in 38.212)
|
||||
// it says: PMI wideband information fields X1 , from left to right
|
||||
int bitlen = csi_report->csi_meas_bitlen.pmi_i13_bitlen[idx];
|
||||
if (layers == 1 && bitlen != 0) {
|
||||
LOG_E(NR_MAC, "Invalid i13 bit length %d for single layer! It should be 0\n", bitlen);
|
||||
return 0;
|
||||
int reported_ri_layers = sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.ri + 1;
|
||||
if (layers != reported_ri_layers) {
|
||||
LOG_D(NR_MAC, "PMI mismatch: using %d layers but RI report is for %d layers, using identity matrix\n",
|
||||
layers, reported_ri_layers);
|
||||
return 0;
|
||||
}
|
||||
const int i13 = x1 & ((1 << bitlen) - 1);
|
||||
x1 >>= bitlen;
|
||||
bitlen = csi_report->csi_meas_bitlen.pmi_i12_bitlen[idx];
|
||||
const int i12 = x1 & ((1 << bitlen) - 1);
|
||||
x1 >>= bitlen;
|
||||
bitlen = csi_report->csi_meas_bitlen.pmi_i11_bitlen[idx];
|
||||
const int i11 = x1 & ((1 << bitlen) - 1);
|
||||
const int i2 = x2;
|
||||
int k1, k2;
|
||||
get_k1_k2_indices(layers, N1, N2, i13, &k1, &k2); // get indices k1 and k2 for PHY matrix (not actual k1 and k2 values)
|
||||
const int O2 = N2 == 1 ? 1 : 4;
|
||||
int O1 = 4; //Horizontal beam oversampling = 4 for more than 2 antenna ports
|
||||
int max_i2 = 0;
|
||||
int lay_index = 0;
|
||||
max_i2 = layers == 1 ? 4 : 2;
|
||||
int K1, K2;
|
||||
get_K1_K2(N1, N2, &K1, &K2, layers);
|
||||
// computing precoding matrix index according to rule set in allocation function init_codebook_gNB
|
||||
lay_index = i2 + (i11 * max_i2) + (i12 * max_i2 * N1 * O1) + (k1 * max_i2 * N1 * O1 * N2 * O2) + (k2 * max_i2 * N1 * O1 * N2 * O2 * K1);
|
||||
return 1 + prev_layers_size + lay_index;
|
||||
}
|
||||
nr_antenna_state_t* ant_state = nrmac->antenna_state;
|
||||
if (ant_state) {
|
||||
if (nr_is_in_recovery_mode(ant_state)) {
|
||||
LOG_D(NR_MAC, "[ANTENNA_MUTE] Recovery mode active, using identity matrix\n");
|
||||
return 0;
|
||||
}
|
||||
// Don't fall back to identity matrix for muted antennas.
|
||||
// Antenna muting is handled at RU level (nr_feptx zeros muted antenna buffers).
|
||||
// Using proper precoding ensures data reaches all active antennas.
|
||||
}
|
||||
|
||||
const int report_id = sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.csi_report_id;
|
||||
const nr_csi_report_t *csi_report = &UE->csi_report_template[report_id];
|
||||
const int N1 = csi_report->N1;
|
||||
const int N2 = csi_report->N2;
|
||||
const int antenna_ports = (N1 * N2) << 1;
|
||||
if (antenna_ports < 2) return 0; // single antenna port
|
||||
|
||||
int x1 = sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.pmi_x1;
|
||||
const int x2 = sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.pmi_x2;
|
||||
LOG_D(NR_MAC,"PMI report: x1 %d x2 %d layers: %d\n", x1, x2, layers);
|
||||
|
||||
int prev_layers_size = 0;
|
||||
for (int i = 1; i < layers; i++)
|
||||
prev_layers_size += nrmac->precoding_matrix_size[i - 1];
|
||||
|
||||
// need to return PM index to matrix initialized in init_DL_MIMO_codebook
|
||||
// index 0 is for identity matrix
|
||||
// order of matrices depends on layers to be transmitted
|
||||
// elements from 1 to n for 1 layer
|
||||
// elements from n+1 to m for 2 layers etc.
|
||||
int pm_index;
|
||||
if (antenna_ports == 2)
|
||||
pm_index = 1 + prev_layers_size + x2; // 0 for identity matrix
|
||||
else {
|
||||
int idx = layers - 1;
|
||||
// the order of i1x in X1 report needs to be verified
|
||||
// the standard is not very clear (Table 6.3.1.1.2-7 in 38.212)
|
||||
// it says: PMI wideband information fields X1 , from left to right
|
||||
int bitlen = csi_report->csi_meas_bitlen.pmi_i13_bitlen[idx];
|
||||
if (layers == 1 && bitlen != 0) {
|
||||
LOG_E(NR_MAC, "Invalid i13 bit length %d for single layer! It should be 0\n", bitlen);
|
||||
return 0;
|
||||
}
|
||||
const int i13 = x1 & ((1 << bitlen) - 1);
|
||||
x1 >>= bitlen;
|
||||
bitlen = csi_report->csi_meas_bitlen.pmi_i12_bitlen[idx];
|
||||
const int i12 = x1 & ((1 << bitlen) - 1);
|
||||
x1 >>= bitlen;
|
||||
bitlen = csi_report->csi_meas_bitlen.pmi_i11_bitlen[idx];
|
||||
const int i11 = x1 & ((1 << bitlen) - 1);
|
||||
const int i2 = x2;
|
||||
int k1, k2;
|
||||
get_k1_k2_indices(layers, N1, N2, i13, &k1, &k2); // get indices k1 and k2 for PHY matrix (not actual k1 and k2 values)
|
||||
const int O2 = N2 == 1 ? 1 : 4;
|
||||
int O1 = 4; //Horizontal beam oversampling = 4 for more than 2 antenna ports
|
||||
int max_i2 = 0;
|
||||
int lay_index = 0;
|
||||
max_i2 = layers == 1 ? 4 : 2;
|
||||
int K1, K2;
|
||||
get_K1_K2(N1, N2, &K1, &K2, layers);
|
||||
// computing precoding matrix index according to rule set in allocation function init_codebook_gNB
|
||||
lay_index = i2 + (i11 * max_i2) + (i12 * max_i2 * N1 * O1) + (k1 * max_i2 * N1 * O1 * N2 * O2) + (k2 * max_i2 * N1 * O1 * N2 * O2 * K1);
|
||||
pm_index = 1 + prev_layers_size + lay_index;
|
||||
}
|
||||
int max_valid_pm = prev_layers_size + nrmac->precoding_matrix_size[layers - 1];
|
||||
if (pm_index < 1 || pm_index > max_valid_pm) {
|
||||
LOG_W(NR_MAC, "PM index %d out of range [1, %d] for %d layers, using identity matrix\n",
|
||||
pm_index, max_valid_pm, layers);
|
||||
return 0;
|
||||
}
|
||||
return pm_index;
|
||||
}
|
||||
|
||||
// look-up table for AMC. Based on BLER vs SNR curves from the nr_dlsim simulation
|
||||
|
||||
@@ -544,7 +544,7 @@ static void evaluate_sinr_report(gNB_MAC_INST *nrmac,
|
||||
stats->num_sinr_meas++;
|
||||
|
||||
const int mcs_table = UE->current_DL_BWP.mcsTableIdx;
|
||||
const int nrOfLayers = get_dl_nrOfLayers(sched_ctrl, UE->current_DL_BWP.dci_format);
|
||||
const int nrOfLayers = get_dl_nrOfLayers(nrmac, sched_ctrl, UE->current_DL_BWP.dci_format);
|
||||
sched_ctrl->dl_max_mcs = get_mcs_from_SINRx10(mcs_table, sinr_report->SINRx10[0], nrOfLayers);
|
||||
|
||||
LOG_D(MAC, "Reported SSB-SINR = %d.%d, dl_max_mcs %d\n", sinr_report->SINRx10[0] / 10, sinr_report->SINRx10[0] % 10, sched_ctrl->dl_max_mcs);
|
||||
|
||||
@@ -399,7 +399,7 @@ uint16_t get_pm_index(const gNB_MAC_INST *nrmac,
|
||||
int get_mcs_from_SINRx10(int mcs_table, int SINRx10, int Nl);
|
||||
uint8_t get_mcs_from_cqi(int mcs_table, int cqi_table, int cqi_idx);
|
||||
|
||||
uint8_t get_dl_nrOfLayers(const NR_UE_sched_ctrl_t *sched_ctrl, const nr_dci_format_t dci_format);
|
||||
uint8_t get_dl_nrOfLayers(const gNB_MAC_INST *mac, const NR_UE_sched_ctrl_t *sched_ctrl, const nr_dci_format_t dci_format);
|
||||
int get_ul_nrOfLayers(const NR_UE_sched_ctrl_t *sched_ctrl, const nr_dci_format_t dci_format);
|
||||
|
||||
void free_sched_pucch_list(NR_UE_sched_ctrl_t *sched_ctrl);
|
||||
|
||||
@@ -769,9 +769,7 @@ void ue_context_modification_request(const f1ap_ue_context_mod_req_t *req)
|
||||
LOG_E(NR_MAC,
|
||||
"RRC reconfiguration outcome unsuccessful, but no rollback mechanism implemented to come back to old configuration\n");
|
||||
} else if (req->reconfig_compl) {
|
||||
LOG_I(NR_MAC, "DU received confirmation of successful RRC Reconfiguration\n");
|
||||
if (UE->reconfigCellGroup) {
|
||||
LOG_W(NR_MAC, "reconfigCellGroup still present, did we miss ACK for RRCReconfiguration?\n");
|
||||
ASN_STRUCT_FREE(asn_DEF_NR_CellGroupConfig, UE->CellGroup);
|
||||
UE->CellGroup = UE->reconfigCellGroup;
|
||||
UE->reconfigCellGroup = NULL;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -645,10 +645,10 @@ static void set_dl_maxmimolayers(NR_PDSCH_ServingCellConfig_t *pdsch_servingcell
|
||||
pdsch_servingcellconfig->ext1->maxMIMO_Layers = calloc(1, sizeof(*pdsch_servingcellconfig->ext1->maxMIMO_Layers));
|
||||
|
||||
long l = ue_supported_dl_layers(scc, uecap);
|
||||
long ue_supported_layers = l > 1 ? l : 2; // min UE supported layers = 2
|
||||
if (maxMIMO_layers == -1)
|
||||
long ue_supported_layers = l > 1 ? l : 2;
|
||||
if (maxMIMO_layers == -1)
|
||||
*pdsch_servingcellconfig->ext1->maxMIMO_Layers = min(NR_MAX_SUPPORTED_DL_LAYERS, ue_supported_layers);
|
||||
else
|
||||
else
|
||||
*pdsch_servingcellconfig->ext1->maxMIMO_Layers = min(maxMIMO_layers, ue_supported_layers);
|
||||
}
|
||||
|
||||
@@ -1287,7 +1287,7 @@ static void config_pucch_resset1(NR_PUCCH_Config_t *pucch_Config,
|
||||
pucchfmt2->interslotFrequencyHopping = NULL;
|
||||
pucchfmt2->additionalDMRS = NULL;
|
||||
pucchfmt2->maxCodeRate = calloc(1,sizeof(*pucchfmt2->maxCodeRate));
|
||||
*pucchfmt2->maxCodeRate = NR_PUCCH_MaxCodeRate_zeroDot15;
|
||||
*pucchfmt2->maxCodeRate = NR_PUCCH_MaxCodeRate_zeroDot25;
|
||||
pucchfmt2->nrofSlots = NULL;
|
||||
pucchfmt2->pi2BPSK = NULL;
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ target_sources(oran_fhlib_5g_mplane PRIVATE
|
||||
subscribe-mplane.c
|
||||
config-mplane.c
|
||||
ru-mplane-api.c
|
||||
antenna-control.c
|
||||
xml/get-xml.c
|
||||
yang/get-yang.c
|
||||
yang/create-yang-config.c
|
||||
|
||||
@@ -0,0 +1,368 @@
|
||||
#include "antenna-control.h"
|
||||
#include "ru-mplane-api.h"
|
||||
#include "xml/get-xml.h"
|
||||
#include "common/utils/assertions.h"
|
||||
|
||||
// Forward declaration - actual function in config-mplane.c
|
||||
extern bool edit_val_commmit_rpc(ru_session_t *ru_session, const char *content);
|
||||
|
||||
// Forward declaration - actual function in get-mplane.c
|
||||
extern bool get_mplane(ru_session_t *ru_session, char **answer);
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <libyang/libyang.h>
|
||||
#include <nc_client.h>
|
||||
|
||||
const char *antenna_ctrl_result_str(antenna_ctrl_result_t result)
|
||||
{
|
||||
switch (result) {
|
||||
case ANTENNA_CTRL_SUCCESS:
|
||||
return "Success";
|
||||
case ANTENNA_CTRL_ERR_NO_SESSION:
|
||||
return "No active NETCONF session";
|
||||
case ANTENNA_CTRL_ERR_FEATURE_NOT_SUPPORTED:
|
||||
return "MPLANE-TRX-CONTROL feature not supported by RU";
|
||||
case ANTENNA_CTRL_ERR_INVALID_ARRAY_NAME:
|
||||
return "Invalid array name";
|
||||
case ANTENNA_CTRL_ERR_INVALID_MASK:
|
||||
return "Invalid antenna bitmask";
|
||||
case ANTENNA_CTRL_ERR_NETCONF_FAILED:
|
||||
return "NETCONF RPC failed";
|
||||
case ANTENNA_CTRL_ERR_VALIDATION_FAILED:
|
||||
return "Configuration validation failed";
|
||||
case ANTENNA_CTRL_ERR_COMMIT_FAILED:
|
||||
return "Configuration commit failed";
|
||||
default:
|
||||
return "Unknown error";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Convert antenna bitmask to O-RAN binary format
|
||||
*
|
||||
* O-RAN uses binary type in YANG which is base64 encoded in XML.
|
||||
* For simplicity, we encode the bitmask as hex bytes.
|
||||
*
|
||||
* Per O-RAN spec: "The antenna mask contains as many binary values as
|
||||
* number of array elements associated with respective tx or rx-array.
|
||||
* Value of '1' indicates antenna element shall be kept active and
|
||||
* value of '0' indicates antenna element should be turned inactive."
|
||||
*/
|
||||
static char *bitmask_to_base64(uint8_t bitmask, uint8_t num_antennas)
|
||||
{
|
||||
// For up to 8 antennas, we need 1 byte
|
||||
// Base64 encoding of 1 byte = 4 characters + null terminator
|
||||
char *result = malloc(8);
|
||||
if (result == NULL) return NULL;
|
||||
|
||||
// Mask to only include configured antennas
|
||||
uint8_t valid_mask = (1 << num_antennas) - 1;
|
||||
uint8_t masked_value = bitmask & valid_mask;
|
||||
|
||||
// Simple base64 encoding for single byte
|
||||
// Input: masked_value (8 bits) -> padded to 6+6+4 bits for base64
|
||||
static const char b64chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
// Encode single byte with padding
|
||||
result[0] = b64chars[(masked_value >> 2) & 0x3F];
|
||||
result[1] = b64chars[(masked_value << 4) & 0x30];
|
||||
result[2] = '=';
|
||||
result[3] = '=';
|
||||
result[4] = '\0';
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Decode base64 to antenna bitmask
|
||||
*/
|
||||
static uint8_t base64_to_bitmask(const char *b64_str)
|
||||
{
|
||||
if (b64_str == NULL || strlen(b64_str) < 2) return 0xFF;
|
||||
|
||||
static const char b64chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
// Find index of first two base64 characters
|
||||
const char *p1 = strchr(b64chars, b64_str[0]);
|
||||
const char *p2 = strchr(b64chars, b64_str[1]);
|
||||
|
||||
if (p1 == NULL || p2 == NULL) return 0xFF;
|
||||
|
||||
uint8_t val1 = (uint8_t)(p1 - b64chars);
|
||||
uint8_t val2 = (uint8_t)(p2 - b64chars);
|
||||
|
||||
// Reconstruct byte
|
||||
return (val1 << 2) | (val2 >> 4);
|
||||
}
|
||||
|
||||
bool mplane_create_antenna_mask_xml(ru_session_t *ru_session,
|
||||
bool is_tx,
|
||||
const char *array_name,
|
||||
uint8_t antenna_bitmask,
|
||||
uint8_t num_antennas,
|
||||
char **xml_content)
|
||||
{
|
||||
if (ru_session == NULL || ru_session->ctx == NULL || array_name == NULL || xml_content == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*xml_content = NULL;
|
||||
LY_ERR ret = LY_SUCCESS;
|
||||
struct lyd_node *root = NULL;
|
||||
|
||||
// Create root node for o-ran-uplane-conf
|
||||
ret = lyd_new_path(NULL, (struct ly_ctx *)ru_session->ctx,
|
||||
"/o-ran-uplane-conf:user-plane-configuration", NULL, 0, &root);
|
||||
if (ret != LY_SUCCESS) {
|
||||
MP_LOG_W("Failed to create root user-plane-configuration node.\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create the antenna mask configuration
|
||||
const char *list_name = is_tx ? "tx-array-antenna-mask-config" : "rx-array-antenna-mask-config";
|
||||
struct lyd_node *mask_node = NULL;
|
||||
|
||||
ret = lyd_new_list(root, NULL, list_name, 0, &mask_node, array_name);
|
||||
if (ret != LY_SUCCESS) {
|
||||
MP_LOG_W("Failed to create %s list node for array '%s'.\n", list_name, array_name);
|
||||
lyd_free_tree(root);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Convert bitmask to base64 for YANG binary type
|
||||
char *b64_mask = bitmask_to_base64(antenna_bitmask, num_antennas);
|
||||
if (b64_mask == NULL) {
|
||||
MP_LOG_W("Failed to encode antenna bitmask.\n");
|
||||
lyd_free_tree(root);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add antenna-bitmask leaf
|
||||
ret = lyd_new_term(mask_node, NULL, "antenna-bitmask", b64_mask, 0, NULL);
|
||||
free(b64_mask);
|
||||
|
||||
if (ret != LY_SUCCESS) {
|
||||
MP_LOG_W("Failed to create antenna-bitmask node.\n");
|
||||
lyd_free_tree(root);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Print to XML
|
||||
// Note: LYD_PRINT_SIBLINGS in libyang v2.1+, LYD_PRINT_WITHSIBLINGS in older v2, LYP_WITHSIBLINGS in v1
|
||||
#ifdef MPLANE_V1
|
||||
lyd_print_mem(xml_content, root, LYD_XML, LYP_WITHSIBLINGS);
|
||||
#elif defined MPLANE_V2
|
||||
#ifdef LYD_PRINT_SIBLINGS
|
||||
lyd_print_mem(xml_content, root, LYD_XML, LYD_PRINT_SIBLINGS);
|
||||
#else
|
||||
lyd_print_mem(xml_content, root, LYD_XML, LYD_PRINT_WITHSIBLINGS);
|
||||
#endif
|
||||
#endif
|
||||
lyd_free_tree(root);
|
||||
|
||||
if (*xml_content == NULL) {
|
||||
MP_LOG_W("Failed to generate XML content.\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
MP_LOG_I("Generated %s antenna mask XML for array '%s', mask=0x%02X\n",
|
||||
is_tx ? "TX" : "RX", array_name, antenna_bitmask);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
antenna_ctrl_result_t mplane_set_tx_antenna_mask(ru_session_t *ru_session,
|
||||
const char *array_name,
|
||||
uint8_t antenna_bitmask,
|
||||
uint8_t num_antennas)
|
||||
{
|
||||
if (ru_session == NULL || ru_session->session == NULL) {
|
||||
return ANTENNA_CTRL_ERR_NO_SESSION;
|
||||
}
|
||||
|
||||
if (array_name == NULL || strlen(array_name) == 0) {
|
||||
return ANTENNA_CTRL_ERR_INVALID_ARRAY_NAME;
|
||||
}
|
||||
|
||||
if (num_antennas == 0 || num_antennas > 8) {
|
||||
return ANTENNA_CTRL_ERR_INVALID_MASK;
|
||||
}
|
||||
|
||||
// Generate XML for antenna mask configuration
|
||||
char *xml_content = NULL;
|
||||
if (!mplane_create_antenna_mask_xml(ru_session, true, array_name,
|
||||
antenna_bitmask, num_antennas, &xml_content)) {
|
||||
return ANTENNA_CTRL_ERR_NETCONF_FAILED;
|
||||
}
|
||||
|
||||
MP_LOG_I("Setting TX antenna mask via NETCONF:\n%s\n", xml_content);
|
||||
|
||||
// Use existing M-Plane infrastructure to send edit-config, validate, commit
|
||||
bool success = edit_val_commmit_rpc(ru_session, xml_content);
|
||||
free(xml_content);
|
||||
|
||||
if (!success) {
|
||||
MP_LOG_W("Failed to configure TX antenna mask for array '%s'\n", array_name);
|
||||
return ANTENNA_CTRL_ERR_COMMIT_FAILED;
|
||||
}
|
||||
|
||||
MP_LOG_I("Successfully configured TX antenna mask 0x%02X for array '%s'\n",
|
||||
antenna_bitmask, array_name);
|
||||
|
||||
return ANTENNA_CTRL_SUCCESS;
|
||||
}
|
||||
|
||||
antenna_ctrl_result_t mplane_set_rx_antenna_mask(ru_session_t *ru_session,
|
||||
const char *array_name,
|
||||
uint8_t antenna_bitmask,
|
||||
uint8_t num_antennas)
|
||||
{
|
||||
if (ru_session == NULL || ru_session->session == NULL) {
|
||||
return ANTENNA_CTRL_ERR_NO_SESSION;
|
||||
}
|
||||
|
||||
if (array_name == NULL || strlen(array_name) == 0) {
|
||||
return ANTENNA_CTRL_ERR_INVALID_ARRAY_NAME;
|
||||
}
|
||||
|
||||
if (num_antennas == 0 || num_antennas > 8) {
|
||||
return ANTENNA_CTRL_ERR_INVALID_MASK;
|
||||
}
|
||||
|
||||
// Generate XML for antenna mask configuration
|
||||
char *xml_content = NULL;
|
||||
if (!mplane_create_antenna_mask_xml(ru_session, false, array_name,
|
||||
antenna_bitmask, num_antennas, &xml_content)) {
|
||||
return ANTENNA_CTRL_ERR_NETCONF_FAILED;
|
||||
}
|
||||
|
||||
MP_LOG_I("Setting RX antenna mask via NETCONF:\n%s\n", xml_content);
|
||||
|
||||
// Use existing M-Plane infrastructure to send edit-config, validate, commit
|
||||
bool success = edit_val_commmit_rpc(ru_session, xml_content);
|
||||
free(xml_content);
|
||||
|
||||
if (!success) {
|
||||
MP_LOG_W("Failed to configure RX antenna mask for array '%s'\n", array_name);
|
||||
return ANTENNA_CTRL_ERR_COMMIT_FAILED;
|
||||
}
|
||||
|
||||
MP_LOG_I("Successfully configured RX antenna mask 0x%02X for array '%s'\n",
|
||||
antenna_bitmask, array_name);
|
||||
|
||||
return ANTENNA_CTRL_SUCCESS;
|
||||
}
|
||||
|
||||
antenna_ctrl_result_t mplane_get_tx_antenna_mask(ru_session_t *ru_session,
|
||||
const char *array_name,
|
||||
uint8_t *antenna_bitmask)
|
||||
{
|
||||
if (ru_session == NULL || ru_session->session == NULL) {
|
||||
return ANTENNA_CTRL_ERR_NO_SESSION;
|
||||
}
|
||||
|
||||
if (array_name == NULL || antenna_bitmask == NULL) {
|
||||
return ANTENNA_CTRL_ERR_INVALID_ARRAY_NAME;
|
||||
}
|
||||
|
||||
// Get operational datastore
|
||||
char *operational_ds = NULL;
|
||||
if (!get_mplane(ru_session, &operational_ds)) {
|
||||
return ANTENNA_CTRL_ERR_NETCONF_FAILED;
|
||||
}
|
||||
|
||||
// Build XPath to find the antenna mask for this array
|
||||
char xpath[256];
|
||||
snprintf(xpath, sizeof(xpath),
|
||||
"tx-array-antenna-mask-config[array-name='%s']/antenna-bitmask",
|
||||
array_name);
|
||||
|
||||
// Extract the value from XML response
|
||||
char *mask_b64 = get_ru_xml_node(operational_ds, "antenna-bitmask");
|
||||
free(operational_ds);
|
||||
|
||||
if (mask_b64 == NULL) {
|
||||
// No mask configured, assume all active
|
||||
*antenna_bitmask = 0xFF;
|
||||
return ANTENNA_CTRL_SUCCESS;
|
||||
}
|
||||
|
||||
*antenna_bitmask = base64_to_bitmask(mask_b64);
|
||||
free(mask_b64);
|
||||
|
||||
MP_LOG_I("Retrieved TX antenna mask 0x%02X for array '%s'\n",
|
||||
*antenna_bitmask, array_name);
|
||||
|
||||
return ANTENNA_CTRL_SUCCESS;
|
||||
}
|
||||
|
||||
antenna_ctrl_result_t mplane_get_rx_antenna_mask(ru_session_t *ru_session,
|
||||
const char *array_name,
|
||||
uint8_t *antenna_bitmask)
|
||||
{
|
||||
if (ru_session == NULL || ru_session->session == NULL) {
|
||||
return ANTENNA_CTRL_ERR_NO_SESSION;
|
||||
}
|
||||
|
||||
if (array_name == NULL || antenna_bitmask == NULL) {
|
||||
return ANTENNA_CTRL_ERR_INVALID_ARRAY_NAME;
|
||||
}
|
||||
|
||||
// Get operational datastore
|
||||
char *operational_ds = NULL;
|
||||
if (!get_mplane(ru_session, &operational_ds)) {
|
||||
return ANTENNA_CTRL_ERR_NETCONF_FAILED;
|
||||
}
|
||||
|
||||
// Extract the value from XML response (simplified - real impl would use XPath)
|
||||
char *mask_b64 = get_ru_xml_node(operational_ds, "antenna-bitmask");
|
||||
free(operational_ds);
|
||||
|
||||
if (mask_b64 == NULL) {
|
||||
// No mask configured, assume all active
|
||||
*antenna_bitmask = 0xFF;
|
||||
return ANTENNA_CTRL_SUCCESS;
|
||||
}
|
||||
|
||||
*antenna_bitmask = base64_to_bitmask(mask_b64);
|
||||
free(mask_b64);
|
||||
|
||||
MP_LOG_I("Retrieved RX antenna mask 0x%02X for array '%s'\n",
|
||||
*antenna_bitmask, array_name);
|
||||
|
||||
return ANTENNA_CTRL_SUCCESS;
|
||||
}
|
||||
|
||||
bool mplane_supports_trx_control(ru_session_t *ru_session)
|
||||
{
|
||||
if (ru_session == NULL || ru_session->session == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if MPLANE-TRX-CONTROL feature is advertised in RU capabilities
|
||||
// This is done by checking the server capabilities from the hello exchange
|
||||
// or by querying the YANG library
|
||||
|
||||
struct nc_session *session = (struct nc_session *)ru_session->session;
|
||||
const char * const *caps = nc_session_get_cpblts(session);
|
||||
|
||||
if (caps == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Look for o-ran-uplane-conf module with MPLANE-TRX-CONTROL feature
|
||||
for (int i = 0; caps[i] != NULL; i++) {
|
||||
if (strstr(caps[i], "o-ran-uplane-conf") != NULL) {
|
||||
// Module is supported, assume feature is enabled
|
||||
// A more complete implementation would query module features
|
||||
MP_LOG_I("RU supports o-ran-uplane-conf module (MPLANE-TRX-CONTROL)\n");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
MP_LOG_W("RU does not advertise o-ran-uplane-conf module\n");
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
#ifndef ANTENNA_CONTROL_H
|
||||
#define ANTENNA_CONTROL_H
|
||||
|
||||
#include "ru-mplane-api.h"
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief O-RAN antenna mask configuration result
|
||||
*/
|
||||
typedef enum {
|
||||
ANTENNA_CTRL_SUCCESS = 0,
|
||||
ANTENNA_CTRL_ERR_NO_SESSION,
|
||||
ANTENNA_CTRL_ERR_FEATURE_NOT_SUPPORTED,
|
||||
ANTENNA_CTRL_ERR_INVALID_ARRAY_NAME,
|
||||
ANTENNA_CTRL_ERR_INVALID_MASK,
|
||||
ANTENNA_CTRL_ERR_NETCONF_FAILED,
|
||||
ANTENNA_CTRL_ERR_VALIDATION_FAILED,
|
||||
ANTENNA_CTRL_ERR_COMMIT_FAILED
|
||||
} antenna_ctrl_result_t;
|
||||
|
||||
/**
|
||||
* @brief Convert result code to string description
|
||||
*/
|
||||
const char *antenna_ctrl_result_str(antenna_ctrl_result_t result);
|
||||
|
||||
/**
|
||||
* @brief Set TX antenna bitmask via NETCONF O1 interface
|
||||
*
|
||||
* Implements O-RAN WG4 MPLANE-TRX-CONTROL feature by sending
|
||||
* NETCONF edit-config RPC to configure tx-array-antenna-mask-config.
|
||||
*
|
||||
* The operation sequence is:
|
||||
* 1. Create XML configuration for o-ran-uplane-conf:tx-array-antenna-mask-config
|
||||
* 2. Send NETCONF <edit-config> to candidate datastore
|
||||
* 3. Send NETCONF <validate>
|
||||
* 4. Send NETCONF <commit>
|
||||
*
|
||||
* @param ru_session Active NETCONF session to the RU
|
||||
* @param array_name O-RAN TX array name (e.g., from tx-arrays list)
|
||||
* @param antenna_bitmask Bitmask where bit N = 1 means antenna N is active
|
||||
* @param num_antennas Number of antennas in the array
|
||||
* @return Result code indicating success or specific error
|
||||
*/
|
||||
antenna_ctrl_result_t mplane_set_tx_antenna_mask(ru_session_t *ru_session,
|
||||
const char *array_name,
|
||||
uint8_t antenna_bitmask,
|
||||
uint8_t num_antennas);
|
||||
|
||||
/**
|
||||
* @brief Set RX antenna bitmask via NETCONF O1 interface
|
||||
*
|
||||
* Same as mplane_set_tx_antenna_mask but for RX arrays.
|
||||
* Configures rx-array-antenna-mask-config in o-ran-uplane-conf.
|
||||
*
|
||||
* @param ru_session Active NETCONF session to the RU
|
||||
* @param array_name O-RAN RX array name (e.g., from rx-arrays list)
|
||||
* @param antenna_bitmask Bitmask where bit N = 1 means antenna N is active
|
||||
* @param num_antennas Number of antennas in the array
|
||||
* @return Result code indicating success or specific error
|
||||
*/
|
||||
antenna_ctrl_result_t mplane_set_rx_antenna_mask(ru_session_t *ru_session,
|
||||
const char *array_name,
|
||||
uint8_t antenna_bitmask,
|
||||
uint8_t num_antennas);
|
||||
|
||||
/**
|
||||
* @brief Get current TX antenna mask from RU via NETCONF
|
||||
*
|
||||
* Sends NETCONF <get> RPC to retrieve current tx-array-antenna-mask-config.
|
||||
*
|
||||
* @param ru_session Active NETCONF session to the RU
|
||||
* @param array_name O-RAN TX array name
|
||||
* @param antenna_bitmask Output: current antenna bitmask
|
||||
* @return Result code indicating success or specific error
|
||||
*/
|
||||
antenna_ctrl_result_t mplane_get_tx_antenna_mask(ru_session_t *ru_session,
|
||||
const char *array_name,
|
||||
uint8_t *antenna_bitmask);
|
||||
|
||||
/**
|
||||
* @brief Get current RX antenna mask from RU via NETCONF
|
||||
*
|
||||
* Sends NETCONF <get> RPC to retrieve current rx-array-antenna-mask-config.
|
||||
*
|
||||
* @param ru_session Active NETCONF session to the RU
|
||||
* @param array_name O-RAN RX array name
|
||||
* @param antenna_bitmask Output: current antenna bitmask
|
||||
* @return Result code indicating success or specific error
|
||||
*/
|
||||
antenna_ctrl_result_t mplane_get_rx_antenna_mask(ru_session_t *ru_session,
|
||||
const char *array_name,
|
||||
uint8_t *antenna_bitmask);
|
||||
|
||||
/**
|
||||
* @brief Check if RU supports MPLANE-TRX-CONTROL feature
|
||||
*
|
||||
* Queries the RU capabilities to verify MPLANE-TRX-CONTROL feature
|
||||
* is advertised (defined in o-ran-wg4-features.yang).
|
||||
*
|
||||
* @param ru_session Active NETCONF session to the RU
|
||||
* @return true if feature is supported, false otherwise
|
||||
*/
|
||||
bool mplane_supports_trx_control(ru_session_t *ru_session);
|
||||
|
||||
/**
|
||||
* @brief Create antenna bitmask XML content for NETCONF edit-config
|
||||
*
|
||||
* Helper function to generate O-RAN compliant XML for antenna mask configuration.
|
||||
* Uses libyang to create proper o-ran-uplane-conf structure.
|
||||
*
|
||||
* @param ru_session Active NETCONF session (for libyang context)
|
||||
* @param is_tx true for TX array, false for RX array
|
||||
* @param array_name O-RAN array name
|
||||
* @param antenna_bitmask Antenna bitmask value
|
||||
* @param num_antennas Number of antennas (for binary encoding)
|
||||
* @param xml_content Output: allocated XML string (caller must free)
|
||||
* @return true on success, false on error
|
||||
*/
|
||||
bool mplane_create_antenna_mask_xml(ru_session_t *ru_session,
|
||||
bool is_tx,
|
||||
const char *array_name,
|
||||
uint8_t antenna_bitmask,
|
||||
uint8_t num_antennas,
|
||||
char **xml_content);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* ANTENNA_CONTROL_H */
|
||||
@@ -545,7 +545,9 @@ static void rfsimulator_readconfig(rfsimulator_state_t *rfsimulator)
|
||||
break;
|
||||
} else if (strcmp(rfsimu_params[p].strlistptr[i], "chanmod") == 0) {
|
||||
init_channelmod();
|
||||
load_channellist(rfsimulator->tx_num_channels,
|
||||
// Use max(tx,rx) for channel model TX dimension so the DL path works
|
||||
// when peer has more TX antennas than our TX (e.g., UE tx=1, gNB tx=4)
|
||||
load_channellist(std::max(rfsimulator->tx_num_channels, rfsimulator->rx_num_channels),
|
||||
rfsimulator->rx_num_channels,
|
||||
rfsimulator->sample_rate,
|
||||
rfsimulator->rx_freq,
|
||||
@@ -649,7 +651,7 @@ static int rfsimu_setchanmod_cmd(char *buff, int debug, telnet_printfunc_t prnt,
|
||||
if (b->channel_model->model_name == NULL)
|
||||
continue;
|
||||
if (b->conn_sock >= 0 && (strcmp(b->channel_model->model_name, modelname) == 0)) {
|
||||
channel_desc_t *newmodel = new_channel_desc_scm(t->tx_num_channels,
|
||||
channel_desc_t *newmodel = new_channel_desc_scm(std::max(t->tx_num_channels, t->rx_num_channels),
|
||||
t->rx_num_channels,
|
||||
static_cast<SCM_t>(channelmod),
|
||||
t->sample_rate,
|
||||
|
||||
Executable
+102
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env bash
|
||||
#!/bin/bash
|
||||
# Continuous traffic generator for OAI UE containers (no intervals, runs virtually forever).
|
||||
# Usage examples:
|
||||
# ./traffic_runner.sh --profile boring
|
||||
# PROFILE=bulk_download ./traffic_runner.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PROFILE="${PROFILE:-}"
|
||||
|
||||
# Fixed server coordinates
|
||||
IPERF_HOST_UDP="192.168.71.130"
|
||||
IPERF_HOST_TCP="192.168.71.130"
|
||||
OAITUN_IP="$(ip -o -4 addr show dev oaitun_ue1 | awk '{print $4}' | cut -d/ -f1)"
|
||||
|
||||
# Per-UE port: derive from last octet of oaitun IP (12.1.1.x → port 5200+x)
|
||||
UE_OCTET="${OAITUN_IP##*.}"
|
||||
IPERF_PORT=$((5200 + UE_OCTET))
|
||||
|
||||
# Very long duration to approximate "forever"
|
||||
LONG_T="${LONG_T:-86400}" # 24h (iperf3 max)
|
||||
|
||||
# -------------- args --------------
|
||||
if [[ $# -gt 0 ]]; then
|
||||
case "$1" in
|
||||
--profile=*) PROFILE="${1#--profile=}"; shift ;;
|
||||
--profile) PROFILE="${2-}"; shift 2 ;;
|
||||
-p=*) PROFILE="${1#-p=}"; shift ;;
|
||||
-p) PROFILE="${2-}"; shift 2 ;;
|
||||
-*) ;; # ignore other flags
|
||||
*) PROFILE="$1"; shift ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
[[ -n "$PROFILE" ]] || { echo "ERROR: --profile is required" >&2; exit 2; }
|
||||
|
||||
iperf_tcp_dl_forever() { ( iperf -c "$IPERF_HOST_TCP" -B "$OAITUN_IP" -p "$IPERF_PORT" -R -t "$LONG_T" >/dev/null 2>&1 ); }
|
||||
iperf_tcp_ul_forever() { ( iperf -c "$IPERF_HOST_TCP" -B "$OAITUN_IP" -p "$IPERF_PORT" -t "$LONG_T" >/dev/null 2>&1 ); }
|
||||
# Use iperf3 for UDP DL — iperf2 -R does not properly pass -b bandwidth to server
|
||||
iperf_udp_dl_forever() { ( iperf3 -u -c "$IPERF_HOST_UDP" -B "$OAITUN_IP" -p "$IPERF_PORT" -R -b $1 -t "$LONG_T" >/dev/null 2>&1 ); }
|
||||
iperf_udp_ul_forever() { ( iperf3 -u -c "$IPERF_HOST_UDP" -B "$OAITUN_IP" -p "$IPERF_PORT" -b $1 -t "$LONG_T" >/dev/null 2>&1 ); }
|
||||
iperf_tcp_dl_parallel_forever() { ( iperf -c "$IPERF_HOST_TCP" -B "$OAITUN_IP" -p "$IPERF_PORT" -R -w 10M -P 8 -t "$LONG_T" >/dev/null 2>&1 ); }
|
||||
|
||||
# ---------------- profiles ----------------
|
||||
do_boring() {
|
||||
# iperf_tcp_dl_parallel_forever
|
||||
iperf_udp_dl_forever 8M
|
||||
wait
|
||||
# iperf -c "$IPERF_HOST" -p "$IPERF_PORT" -B "$OAITUN_IP" -u -b 25M -t "$LONG_T"
|
||||
}
|
||||
|
||||
do_browsing() {
|
||||
iperf_tcp_dl_forever
|
||||
iperf_tcp_ul_forever
|
||||
wait
|
||||
}
|
||||
|
||||
do_video() {
|
||||
iperf_udp_dl_forever 6M
|
||||
wait
|
||||
}
|
||||
|
||||
do_bulk_download() {
|
||||
iperf -c "$IPERF_HOST" -p "$IPERF_PORT" -B "$OAITUN_IP" -R -P 8 -w 1M -t "$LONG_T"
|
||||
wait
|
||||
}
|
||||
|
||||
do_voip() {
|
||||
iperf -c "$IPERF_HOST" -p "$IPERF_PORT" -B "$OAITUN_IP" -R -u -b 80K -t "$LONG_T";
|
||||
iperf -c "$IPERF_HOST" -p "$IPERF_PORT" -B "$OAITUN_IP" -u -b 80K -t "$LONG_T";
|
||||
wait
|
||||
}
|
||||
|
||||
do_iot_chatty() {
|
||||
iperf -c "$IPERF_HOST" -p "$IPERF_PORT" -B "$OAITUN_IP" -R -u -b 40K -t "$LONG_T";
|
||||
iperf -c "$IPERF_HOST" -p "$IPERF_PORT" -B "$OAITUN_IP" -u -b 20K -t "$LONG_T";
|
||||
wait
|
||||
}
|
||||
|
||||
do_gaming() {
|
||||
iperf -c "$IPERF_HOST" -p "$IPERF_PORT" -B "$OAITUN_IP" -R -u -b 120K -t "$LONG_T";
|
||||
iperf -c "$IPERF_HOST" -p "$IPERF_PORT" -B "$OAITUN_IP" -u -b 10K -t "$LONG_T";
|
||||
wait
|
||||
}
|
||||
|
||||
do_idle() {
|
||||
tail -f /dev/null
|
||||
}
|
||||
|
||||
# -------------- dispatch --------------
|
||||
case "$PROFILE" in
|
||||
boring) do_boring;;
|
||||
browsing) do_browsing;;
|
||||
video) do_video;;
|
||||
bulk_download) do_bulk_download;;
|
||||
voip) do_voip;;
|
||||
iot_chatty) do_iot_chatty;;
|
||||
gaming) do_gaming;;
|
||||
idle) do_idle;;
|
||||
*) echo "Unknown profile: $PROFILE" >&2; exit 1;;
|
||||
esac
|
||||
Reference in New Issue
Block a user