18 Commits
Author SHA1 Message Date
maki f8178cede9 mac: Phase 4 - cap MU-MIMO group size to active TX antenna count
Reads mac->antenna_state to count active TX ports via
__builtin_popcount(nr_get_tx_antenna_bitmask). Caps the partner-search
while-loop bound to (active_tx - 1), so:
  active_tx = 4 -> up to 4-way MU-MIMO  (3 partners)
  active_tx = 3 -> up to 3-way          (2 partners)
  active_tx = 2 -> up to 2-way          (1 partner)
  active_tx = 1 -> SU-MIMO only         (0 partners)

This couples antenna muting (the existing PHY-side socket control on
port 9999) to spatial parallelism in the MU-MIMO scheduler. Muting a
chain reduces the codebook's orthogonal codeword count, which is the
right semantic for our antenna-muting research: muting reduces the
number of UEs co-scheduled per slot, not the per-UE rate.

Verified runtime behavior on rfsim (30s windows per state):
  TX mask 0x0F:  4-way 952  3-way 484  2-way 333  (full 4-TX)
  TX mask 0x03:  4-way   2  3-way   8  2-way 5935 (2-TX -> 2-way)
  TX mask 0x01:  4-way   0  3-way   0  2-way   11 (1-TX -> SU-MIMO)

Antenna-control commands tested via existing test interface:
  echo "SET TX MASK 0x03" | nc -w 2 localhost 9999
2026-04-30 08:27:47 +09:00
maki e1f315bee9 mac: Phase 2 N-way MU-MIMO co-scheduling (up to 4 UEs/slot)
Extends the partner search from 1 partner to up to MAX_MU_MIMO_PARTNERS=3
partners, totalling 4 UEs per slot — one per orthogonal x2 class of the
4-port Type I rank-1 codebook.

Orthogonality test relaxed: any distinct x2 (with same x1) is accepted.
The 4 codewords w_(x1,n) for n=0..3 are mutually orthogonal because
they share spatial vector v_x1 and differ only in co-phasing j^n;
<w_a, w_b> = 0 for any a != b.

Each partner gets a distinct DMRS port:
  leader    -> port 1000 (CDM group 0, mask 0x1)
  partner 0 -> port 1001 (CDM group 0, mask 0x2)
  partner 1 -> port 1002 (CDM group 1, mask 0x4)
  partner 2 -> port 1003 (CDM group 1, mask 0x8)
numDmrsCdmGrpsNoData=2 reserves both DMRS groups.

Class-uniqueness tracked via classes_used bitmask: each successive
partner must be from an unused x2 class.

Pair log: "[nakaolab MU-MIMO] F.S N-way pair RNTI A (x2=0) + RNTI B
(x2=1) + ... on RBs [..]"

Result on rfsim with 9 UEs cycling 4 classes:
  8069 total pairings observed
   920 4-way (all 4 classes co-scheduled)
  2756 3-way
  4404 2-way
2026-04-30 08:08:55 +09:00
maki df26524d80 mac+rfsim: working MU-MIMO DL co-scheduling with 4-class channel diversity
Phase 1 (rfsim): per-UE deterministic channel injection.
  Each UE container sets OAI_UE_CHANNEL_CLASS=k (k in {0..3}). The rfsim
  no-chanmod sample-mixing path applies a 4x4 channel:
    H^(k)[r,a] = H_orig[r,a] * w_k[a]
  where w_k is the k-th codeword of the Type I 4-port rank-1 codebook
  (N1=2, N2=1). Multiplying by w_k rotates the channel's optimal PMI
  direction to k while preserving the original symmetric kernel's
  full-rank reception (so SSB sync still works on all UE classes).
  10 UEs cycle through 4 classes (UE_index mod 4), giving 4 mutually
  orthogonal PMI populations.

Phase 3 (scheduler): pf_dl partner search now scans connected_ue_list
  (not just UE_sched). After leader UE A is allocated, iterate over all
  connected UEs, run update_dlsch_buffer/dlsch_to_schedule per candidate,
  and pair the first orthogonal-PMI partner that has data + free CCE/PUCCH.
  This bypasses PF time-multiplexing — multiple UEs needn't be in
  UE_sched simultaneously for MU-MIMO to fire.

Per-slot pairing tag (mumimo_paired_slot in NR_UE_sched_ctrl_t) prevents
double-allocation: incremented per pf_dl call, leader and partner both
tagged on success; subsequent leader iterations skip already-tagged UEs.

Results on rfsim with 10 UEs cycling 4 classes (10-min run):
  17000+ pairings observed
  Distribution by (leader_class, partner_class):
    0->2: 6261 (XOR=2)
    3->1: 4382 (XOR=2)
    2->0: 2765 (XOR=2)
    1->3: 1920 (XOR=2)
  All pairs strictly orthogonal per Type I codebook rank-1 rule
  (same x1, x2 differ by 2). 2-way MU-MIMO working end-to-end.

Also includes:
- ri_bitlen=0 fix in evaluate_cqi_report (uninitialized r_index)
- OAI_MU_MIMO=1 env to enable, OAI_MU_MIMO_RELAXED=1 for relaxed test
- Diagnostic logs every 200th call: ENTER + per-skip-reason
2026-04-29 23:00:31 +09:00
maki c5a3ef8b60 mac: MU-MIMO diagnostic instrumentation + ri_bitlen=0 fix
Two changes on top of the initial MU-MIMO patch (commit 766d0a4):

1. CSI-parse fix in evaluate_cqi_report path: when maxMIMO_layers=1
   (and thus ri_Restriction has only one bit set, ri_bitlen=0),
   evaluate_ri_report was being skipped, leaving r_index=-1 which
   trips the DevAssert(ri>=0) inside evaluate_cqi_report. Fix: when
   ri_bitlen=0, pick the single allowed RI directly from the
   restriction bitmap. Affects all three reportQuantity branches
   (cri_RI_PMI_CQI, cri_RI_LI_PMI_CQI, cri_RI_CQI).

2. MU-MIMO scheduler: relaxed-orthogonality flag and broader
   partner search:
   - OAI_MU_MIMO_RELAXED=1 bypasses the strict (same-x1, x2-diff-2)
     orthogonality test for testbed validation on rfsim where the
     symmetric channel makes all UEs report identical PMI.
   - Partner search now scans the full UE_sched (skipping leader and
     already-paired entries) instead of only iterator+1; this finds
     candidates regardless of PF position.
   - Diagnostic LOG_W messages on partner-search entry and per-skip
     reason (capped to 20 events to avoid log flood).

Findings on this w17 build with 6 UEs at 5Mbps DL each:
- Patch compiles, deploys, gNB does not crash, scheduler reaches
  partner-search block on a fraction of slots.
- However eligible_others=0 in every observed partner-search entry:
  PF puts only one UE in UE_sched per pf_dl call almost always,
  even at cell saturation. The other attached UEs are skipped
  earlier (likely HARQ pids pending feedback). So the partner
  search has nothing to pair.
- Architectural conclusion: forcing concurrent multi-UE scheduling
  on OAI's PF + finite-HARQ scheduler likely needs scanning
  connected_ue_list (not just UE_sched), and may need broader
  refactoring to pre-allocate slot resources for MU-MIMO pairs.
2026-04-29 15:09:54 +09:00
maki 766d0a4bd3 mac: MU-MIMO DL co-scheduling for orthogonal-PMI UEs (nakaolab)
Adds opt-in MU-MIMO downlink co-scheduling to the production
proportional-fair scheduler (pf_dl). When OAI_MU_MIMO=1 is set in
the gNB process env, after the leader UE is allocated we look ahead
in the PF-sorted candidate list for an orthogonal-PMI partner. If
found and the partner can secure its own CCE+PUCCH, both UEs share
the same RB allocation with distinct DMRS CDM groups (port 1000 vs
port 1001) and are forced to the same MCS/TBS.

Orthogonality test (4-port Type I rank-1 codebook, N1=2, N2=1):
same x1 (same beam), x2 differing by 2 (co-phasing pairs {0,2} or
{1,3}). Both UEs must report rank-1.

Default OFF — set OAI_MU_MIMO=1 to enable. Builds on the upstream
FAPI/PHY MU-MIMO plumbing (commits 3707d52c95, 498d241d75,
9952996c5d) which carries antenna-port and DMRS-CDM metadata down
to the PHY transmit path.
2026-04-29 13:09:30 +09:00
makiandClaude Opus 4.7 d7d67277f3 Bring over mplane/ helpers required by Dockerfile.nakaolab.test
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 06:55:46 +09:00
makiandClaude Opus 4.7 da224f6ba1 Bring over Dockerfile.nakaolab.test for nakaolab:latest builds
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 06:54:26 +09:00
makiandClaude Opus 4.7 7cd72323af Merge antenna muting mechanism onto 2026.w17
Brings the O-RAN compliant antenna muting feature (originally introduced
on the 'new' branch) onto the latest upstream 2026.w17 tag.

Files added:
  openair1/PHY/NR_TRANSPORT/nr_antenna_state.{c,h}
  openair1/PHY/NR_TRANSPORT/nr_antenna_control.{c,h}

Files modified (integration hooks):
  CMakeLists.txt, executables/nr-gnb.c
  openair1/PHY/INIT/nr_init.c, openair1/PHY/defs_gNB.h
  openair1/PHY/MODULATION/{beamforming.c,nr_modulation.h,nr_beamforming.c}
  openair1/SCHED_NR/nr_ru_procedures.c
  openair2/LAYER2/NR_MAC_gNB/mac_proto.h
  openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_{primitives,dlsch,uci}.c

Conflicts resolved:
  - defs_gNB.h: kept upstream's new headers + appended antenna_state.h
  - nr_init.c: dropped reference to upstream-removed nr_srs_info init,
    kept nr_init_antenna_state() call
  - beamforming.c: upstream moved nr_beam_precoding into new file
    nr_beamforming.c; nr_beam_precoding_with_muting added there with
    upstream's signature (no slot param)
  - nr_modulation.h: aligned _with_muting decl to upstream signature
  - nr_ru_procedures.c: muting check applied around upstream's new
    fft_shift in nr_feptx, and around per-port memcpy in nr_feptx_prec
  - gNB_scheduler_uci.c: SINR-based MCS hint uses get_dl_nrOfLayers(NULL,
    sched_ctrl, ...) since nrmac is not in scope at the call site
  - mac_proto.h: get_dl_nrOfLayers signature updated to take gNB_MAC_INST*

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 06:25:57 +09:00
Jaroslava Fiedlerova bfb177c38e Merge branch 'integration_2026_w17' into 'develop'
Integration `2026.w17`

* !4054 Minor fixes to gNB/UE behavior in RFsim
* !4062 Minor fixes/cleanup in NGAP lib
* !4036 Fix CSI-RS estimation
* !4055 fix (RRC): do not trigger N2 HO when no active PDU sessions are present
* !3988 asn1c inside local tree
* !3762 L1 gNB type0 PDSCH
* !4069 ctest: fix tests when compiling with sanitizers, ignore build directories, fix spsc_q build
* !4067 Small compilation fixes
* !3444 7.2 FHI with XRAN K release
* !4061 T-Tracer & Data Recording v1.1: UL PHY trace modularization and recording enhancements
* !3975 ZMQ radio
* Increase PRACH queue capacity from 8 to 16
* !4059 CI: Various CI adjustments

Closes #1074 and #1073

See merge request oai/openairinterface5g!4063
2026-04-24 20:23:02 +00:00
Jaroslava Fiedlerova 434cd8ca3d Merge remote-tracking branch 'origin/ci-adjustments' into integration_2026_w17 (!4059)
CI: Various CI adjustments

- RAN-SA-FHI72-CN5G: activate/inactive carriers on VVDN RU - to check if
  it improves reliability and stability of the pipeline
- RAN-SA-FHI72-CN5G: update CN with new IP range - avoid IP address
  overlap with CN used in RAN-SA-AERIAL-XXX pipelines
- RAN-SA-B200-Module-SABOX-Container: test with deltaMCS in SC-FDMA test
  case
- RAN-NSA-B200-Module-LTEBOX-Container: adjust PRACH DTX threshold to
  reduce number of false RA attempts on eNB
- remove retx check in RAN-SA-Multi-Antenna-CN5G and RAN-SA-AERIAL-CN5G -
  to avoid false CI failure caused by unsuccessful retx check of
  interfering UE
- adjust RAN-SA-Multi-Antenna-CN5G and RAN-SA-AERIAL-CN5G configs to
  improve UL throughput
2026-04-24 20:46:46 +02:00
Jaroslava Fiedlerova 38167e50f8 CI: Adjust config file in UL heavy test of SA-AERIAL-CN5G
Manual testing showed that increasing pusch_TargetSNRx10 by 3 dB improves
uplink throughput in UL-heavy test cases of SA-AERIAL-CN5G
2026-04-24 17:18:28 +02:00
Jaroslava Fiedlerova d1129d1f90 CI: Reduce RX gain in 100 MHz test of SA-Multi-Antenna-CN5G
Manual testing showed that lowering the RX gain reduces UL
retransmissions and improves overall UL throughput in the 100 MHz 2x2
test case of the SA-Multi-Antenna CN5G pipeline.
2026-04-24 17:18:28 +02:00
Jaroslava Fiedlerova 6a6f200790 CI: Change OC CN for RAN-SA-FHI72-CN5G 2026-04-24 17:18:28 +02:00
Jaroslava Fiedlerova 1c26f372aa CI: Disable machine performance tuning in RAN-SA-FHI72-CN5G
Performance tuning is done automatically after the reboot
2026-04-24 17:18:28 +02:00
Jaroslava Fiedlerova 0da440017e CI: Test deltaMCS power control mode in SA-B200-Module-SABOX-Container 2026-04-24 17:18:28 +02:00
Jaroslava Fiedlerova e0a2b4837c CI: Configuration file adjustment for NSA-B200-Module-LTEBOX-Container
Try to reduce number of false RA attempts on eNB
2026-04-24 17:18:28 +02:00
Jaroslava Fiedlerova 11dc86f59c CI: Add scripts to (in)activate carriers on VVDN
Add vvdn-activate-carriers.sh to verify RU PTP synchronization and
configuration. Activates carriers on VVDN RU by editing sysrepo running
datastore (INACTIVE->ACTIVE).

Add vvdn-inactive-carriers.sh to reverse the operation, setting all
carriers back to INACTIVE via sysrepocfg.
2026-04-24 17:18:28 +02:00
Jaroslava Fiedlerova 547f683ebe CI: Remove retx check from OTA setups with Quectel
Retx check frequently fails in OTA pipelines, particularly for interfering
UEs attempting to connect to the cell. This leads to job failures in OTA
pipelines.

Remove the retx check from  Multi-Antenna and Aerial pipelines to improve
CI stability and avoid false failures. In these pipelines, iperf test
already validates correct funtionality, making the retx check redundant.
2026-04-17 14:09:22 +02:00
38 changed files with 2641 additions and 120 deletions
+2
View File
@@ -919,6 +919,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/LTE_TRANSPORT/prach.c
)
@@ -998,6 +999,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
+4 -4
View File
@@ -79,11 +79,11 @@ oc-cn5g:
oc-cn5g-20897:
Host: cacofonix
NetworkScript: echo "inet 172.21.6.105"
NetworkScript: echo "inet 172.21.6.115"
RunIperf3Server: False
Deploy: "! scripts/oc-cn5g-deploy.sh /opt/oai-cn5g-fed-develop-2025-jan oaicicd-core-for-fhi72"
Undeploy: "! scripts/oc-cn5g-undeploy.sh /opt/oai-cn5g-fed-develop-2025-jan oaicicd-core-for-fhi72"
LogCollect: "! scripts/oc-cn5g-logcollect.sh /opt/oai-cn5g-fed-develop-2025-jan oaicicd-core-for-fhi72 %%log_dir%%"
Deploy: "! scripts/oc-cn5g-deploy.sh /opt/oai-cn5g-fed-develop-2026-apr oaicicd-core-for-fhi72"
Undeploy: "! scripts/oc-cn5g-undeploy.sh /opt/oai-cn5g-fed-develop-2026-apr oaicicd-core-for-fhi72"
LogCollect: "! scripts/oc-cn5g-logcollect.sh /opt/oai-cn5g-fed-develop-2026-apr oaicicd-core-for-fhi72 %%log_dir%%"
oc-cn5g-00103-ho:
Host: groot
@@ -215,6 +215,7 @@ L1s =
(
{
num_cc = 1;
prach_dtx_threshold = 200;
tr_n_preference = "local_mac";
}
);
@@ -186,10 +186,10 @@ MACRLCs = (
local_s_portd = 50011; // vnf p7 port [!]
tr_s_preference = "aerial";
tr_n_preference = "local_RRC";
pucch_RSSI_Threshold = -270;
pusch_RSSI_Threshold = -260;
pusch_TargetSNRx10 = 260; # 150;
pucch_TargetSNRx10 = 200; #200;
pucch_RSSI_Threshold = -270;
pusch_RSSI_Threshold = -260;
pusch_TargetSNRx10 = 290;
pucch_TargetSNRx10 = 200;
dl_max_mcs = 28;
ul_max_mcs = 28;
ul_min_mcs = 9;
@@ -161,7 +161,7 @@ gNBs =
////////// AMF parameters:
amf_ip_address = ({ ipv4 = "172.21.6.103"; });
amf_ip_address = ({ ipv4 = "172.21.6.113"; });
NETWORK_INTERFACES :
{
@@ -197,7 +197,7 @@ RUs = (
nb_tx = 2;
nb_rx = 2;
att_tx = 0;
att_rx = 0;
att_rx = 6;
bands = [77];
max_pdschReferenceSignalPower = -27;
max_rxgain = 71;
@@ -18,7 +18,7 @@ gNBs =
nr_cellid = 12345678L;
////////// Physical parameters:
use_deltaMCS = 1;
servingCellConfigCommon = (
{
@@ -180,7 +180,7 @@ MACRLCs = (
{
tr_s_preference = "local_L1";
tr_n_preference = "local_RRC";
pusch_TargetSNRx10 = 200;
pusch_TargetSNRx10 = 100;
pucch_TargetSNRx10 = 200;
ul_prbblack_SNR_threshold = 10;
}
+48
View File
@@ -0,0 +1,48 @@
#!/bin/sh
# SPDX-License-Identifier: MIT
set -e
RU_IP="10.10.0.111"
RU_HOST="root@10.10.0.111"
ssh_exec() {
ssh $RU_HOST $@ </dev/null
}
echo "→ Checking for PTP fluctuations in the last 60 minutes..."
logs=$(journalctl -u ptp4l.service --since "60 minutes ago")
check_ptp=$(echo "$logs" | awk '
/rms/ {
rms=$8
if (rms > 100) {
print "✗ PTP FLUCTUATION DETECTED → " $0
exit 0
}
}
')
if [ -n "$check_ptp" ]; then
echo "$check_ptp"
else
echo "✓ No PTP fluctuations detected."
fi
echo "→ Checking if VVDN LPRU is PTP synchronized (timeout 1 min)..."
if ssh_exec 'timeout 1m sh -c "tail -F /var/log/synctimingptp2.log | grep -m 1 -F ,\ synchronized"'; then
echo "✓ VVDN LPRU synchronized"
else
echo "✗ VVDN LPRU not synchronized"
fi
echo "→ Checking TX/RX carrier configuration..."
if ssh_exec 'sysrepocfg -X -d running -f xml | grep -q "<active>INACTIVE</active>"'; then
echo "→ Some carriers are not active, activating via sysrepocfg..."
ssh_exec 'sysrepocfg -X -d running -f xml | sed "s/<active>INACTIVE<\/active>/<active>ACTIVE<\/active>/g" | sysrepocfg -I -d running -f xml'
echo "✓ All TX/RX carriers activated."
else
echo "✓ All TX/RX carriers already activated."
fi
exit 0
+14
View File
@@ -0,0 +1,14 @@
#!/bin/sh
# SPDX-License-Identifier: MIT
set -e
RU_HOST="root@10.10.0.111"
ssh_exec() {
ssh $RU_HOST $@ </dev/null
}
echo "→ Inactivating TX/RX carriers on VVDN LPRU..."
ssh_exec 'sysrepocfg -X -d running -f xml | sed "s/ACTIVE/INACTIVE/g" | sysrepocfg -I -d running -f xml'
echo "✓ TX/RX carriers inactivated."
@@ -122,7 +122,7 @@
<node>gracehopper1-oai</node>
<yaml_path>ci-scripts/yaml_files/sa_gnb_aerial</yaml_path>
<analysis>
<services>oai-gnb-aerial=RetxCheck=20,100,100,100 oai-gnb-aerial=EndsWithBye</services>
<services>oai-gnb-aerial=EndsWithBye</services>
</analysis>
</testCase>
@@ -111,7 +111,7 @@
<node>gracehopper1-oai</node>
<yaml_path>ci-scripts/yaml_files/sa_gnb_aerial_ul</yaml_path>
<analysis>
<services>oai-gnb-aerial=RetxCheck=20,100,100,100 oai-gnb-aerial=EndsWithBye</services>
<services>oai-gnb-aerial=EndsWithBye</services>
</analysis>
</testCase>
@@ -12,6 +12,13 @@
<script>yaml_files/sa_fhi_7.2_vvdn_gnb/setup_config.sh</script>
</testCase>
<testCase>
<class>Custom_Script</class>
<desc>Activate carriers on VVDN RU</desc>
<node>cacofonix</node>
<script>scripts/vvdn-activate-carriers.sh</script>
</testCase>
<testCase>
<class>Pull_Local_Registry</class>
<desc>Pull Images from Local Registry</desc>
@@ -134,9 +141,9 @@
<testCase>
<class>Custom_Script</class>
<always_exec>true</always_exec>
<desc>Set CPU to idle state, set kernel parameters to default values</desc>
<desc>Inactivate carriers on VVDN RU</desc>
<node>cacofonix</node>
<script>yaml_files/sa_fhi_7.2_vvdn_gnb/setup_cleanup.sh</script>
<script>scripts/vvdn-inactivate-carriers.sh</script>
</testCase>
</testCaseList>
@@ -104,7 +104,7 @@
<node>matix</node>
<yaml_path>ci-scripts/yaml_files/5g_sa_n310_2x2_100MHz</yaml_path>
<analysis>
<services>oai-gnb=RetxCheck=dl=10,100,100,100;ul=25,100,100,100 oai-gnb=EndsWithBye</services>
<services>oai-gnb=EndsWithBye</services>
</analysis>
</testCase>
@@ -104,7 +104,7 @@
<node>matix</node>
<yaml_path>ci-scripts/yaml_files/5g_sa_n310_2x2_60MHz</yaml_path>
<analysis>
<services>oai-gnb=RetxCheck=dl=15,100,100,100;ul=20,100,100,100 oai-gnb=EndsWithBye</services>
<services>oai-gnb=EndsWithBye</services>
</analysis>
</testCase>
@@ -104,7 +104,7 @@
<node>matix</node>
<yaml_path>ci-scripts/yaml_files/5g_sa_n310_4x4_60MHz</yaml_path>
<analysis>
<services>oai-vnf=RetxCheck=10,100,100,100 oai-vnf=EndsWithBye oai-pnf=EndsWithBye</services>
<services>oai-vnf=EndsWithBye oai-pnf=EndsWithBye</services>
</analysis>
</testCase>
@@ -1,7 +0,0 @@
# SPDX-License-Identifier: MIT
set -e
sudo cpupower idle-set -E > /dev/null
sudo sysctl kernel.sched_rt_runtime_us=950000
sudo sysctl kernel.timer_migration=1
exit 0
@@ -17,7 +17,7 @@ services:
devices:
- /dev/bus/usb/:/dev/bus/usb/
volumes:
- ../../conf_files/gnb.sa.band78.106prb.usrpb200.sc-fdma.conf:/opt/oai-gnb/etc/gnb.conf
- ../../conf_files/gnb.sa.band78.106prb.usrpb200.sc-fdma-deltaMCS.conf:/opt/oai-gnb/etc/gnb.conf
# for performance reasons, we use host mode: in bridge mode, we have
# unacceptable DL TCP performance. However, the whole point of
# containerization is to not be in host mode, so update this to macvlan
+60
View File
@@ -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"]
+10
View File
@@ -24,6 +24,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"
@@ -372,6 +374,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");
}
}
}
+26
View File
@@ -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 "$@"
+602
View File
@@ -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()
+17
View File
@@ -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=="}
]
}
}
]
}
+3
View File
@@ -143,6 +143,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);
/* Do NOT allocate per-antenna rxdataF: the gNB gets a pointer to the
* RU to copy/recover freq-domain memory from there */
common_vars->rxdataF = (c16_t ***)malloc16(common_vars->num_beams_period * sizeof(c16_t**));
+1
View File
@@ -10,6 +10,7 @@
#include "PHY/LTE_TRANSPORT/transport_eNB.h"
#include "modulation_eNB.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,
+32
View File
@@ -4,6 +4,8 @@
#include "nr_modulation.h"
#include "openair1/PHY/TOOLS/tools_defs.h"
#include "PHY/NR_TRANSPORT/nr_antenna_state.h"
#include "common/utils/LOG/log.h"
void nr_beam_precoding(c16_t **txdataF,
c16_t **txdataF_BF,
@@ -25,3 +27,33 @@ 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 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)) {
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);
}
}
+27
View File
@@ -84,6 +84,33 @@ 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] 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 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,
bool is_flat_buff,
@@ -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 */
+3
View File
@@ -21,6 +21,7 @@
#include "common/utils/threadPool/task_ans.h"
#include "openair1/PHY/defs_RU.h"
#include "common/utils/ds/spsc_q.h"
#include "PHY/NR_TRANSPORT/nr_antenna_state.h"
#define MAX_NUM_RU_PER_gNB 8
#define MAX_PUCCH0_NID 8
@@ -430,6 +431,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 -11
View File
@@ -21,6 +21,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)
@@ -165,12 +173,20 @@ 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],
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 {
memcpy((void *)ru->common.txdataF_BF[tx_idx],
(void *)gNB->common_vars.txdataF[b][i],
fp->samples_per_slot_wCP * sizeof(int32_t));
}
}
}
} else {
@@ -205,19 +221,38 @@ 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) {
// FFT shift
const NR_DL_FRAME_PARMS *fp = &ru->gNB_list[0]->frame_parms;
fft_shift(ru->gNB_list[0]->common_vars.txdataF[bb][aa],
fp->ofdm_symbol_size,
fp->N_RB_DL,
(c16_t *)ru->common.txdataF_BF[tx_idx],
fp->ofdm_symbol_size,
startSymbol,
numSymbols);
const nr_antenna_state_t *ant_state = &ru->gNB_list[0]->antenna_state;
if (!nr_is_tx_antenna_active(ant_state, aa)) {
// Antenna is muted - zero out the FFT-shift output (transmission blanking)
memset((void *)&ru->common.txdataF_BF[tx_idx][startSymbol * fp->ofdm_symbol_size], 0,
numSymbols * fp->ofdm_symbol_size * sizeof(int32_t));
} else {
// FFT shift
fft_shift(ru->gNB_list[0]->common_vars.txdataF[bb][aa],
fp->ofdm_symbol_size,
fp->N_RB_DL,
(c16_t *)ru->common.txdataF_BF[tx_idx],
fp->ofdm_symbol_size,
startSymbol,
numSymbols);
}
} else {
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);
@@ -420,7 +420,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
@@ -565,7 +565,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);
@@ -594,8 +593,57 @@ typedef struct UEsched_s {
float coef;
NR_UE_info_t * UE;
int selected_mcs;
bool mumimo_paired; /* nakaolab: already allocated as MU-MIMO partner; main loop must skip */
} UEsched_t;
/* nakaolab: cached MU-MIMO enable state.
* Read once via OAI_MU_MIMO env var. 0 = SU-MIMO (default), 1 = MU-MIMO co-scheduling. */
static int mumimo_enabled(void)
{
static int cached = -1;
if (cached < 0) {
const char *e = getenv("OAI_MU_MIMO");
cached = (e && atoi(e) > 0) ? 1 : 0;
if (cached)
LOG_W(NR_MAC, "[nakaolab] MU-MIMO downlink scheduling ENABLED (OAI_MU_MIMO=1)\n");
}
return cached;
}
/* nakaolab: orthogonal-PMI test for 4-port Type I rank-1 codebook (N1=2, N2=1).
*
* The 4 codewords sharing the same spatial beam (same pmi_x1) are constructed
* with mutually orthogonal co-phasings (theta_n in {1, j, -1, -j}):
* w_(x1, n) = (1/sqrt(4)) * [v_x1 ; j^n * v_x1]
* Pairwise inner product <w_(x1,a), w_(x1,b)> for any a != b is zero, so all
* 4 codewords with the same x1 are mutually orthogonal. Up to 4 UEs (one per
* x2 class) can be co-scheduled simultaneously.
*
* Two UEs with different x1 use different spatial beams. With OAI default
* oversampling (O1=4) the DFT vectors are not strictly orthogonal, so we
* conservatively don't pair across x1.
*
* OAI_MU_MIMO_RELAXED=1 accepts any rank-1 pair (used to validate the
* scheduler path on a symmetric rfsim channel where PMIs collapse). */
static bool pmi_is_orthogonal(const NR_UE_info_t *a, const NR_UE_info_t *b)
{
const struct CRI_RI_LI_PMI_CQI *ra = &a->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report;
const struct CRI_RI_LI_PMI_CQI *rb = &b->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report;
if (ra->ri != 0 || rb->ri != 0)
return false;
static int relaxed = -1;
if (relaxed < 0) {
const char *e = getenv("OAI_MU_MIMO_RELAXED");
relaxed = (e && atoi(e) > 0) ? 1 : 0;
}
if (relaxed)
return true;
if (ra->pmi_x1 != rb->pmi_x1)
return false;
/* Same beam, distinct co-phasing -> mutually orthogonal codewords. */
return ra->pmi_x2 != rb->pmi_x2;
}
static int comparator(const void *p, const void *q)
{
const UEsched_t *pp = p;
@@ -700,7 +748,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,
@@ -730,12 +778,31 @@ static void pf_dl(gNB_MAC_INST *mac,
qsort(UE_sched, numUE, sizeof(UEsched_t), comparator);
UEsched_t *iterator = UE_sched;
/* nakaolab: tag to mark UEs already paired in this pf_dl call. Increments each
* call so a UE's stored tag != current tag means "available for pairing". */
static uint64_t mumimo_pf_call_id = 0;
if (mumimo_enabled())
mumimo_pf_call_id++;
/* nakaolab: diagnostic — log numUE > 1 to understand MU-MIMO opportunity */
if (mumimo_enabled() && numUE >= 2) {
static int dump_count = 0;
if (dump_count++ < 20)
LOG_W(NR_MAC, "[nakaolab MU-MIMO] %4d.%2d numUE=%d (PF candidates)\n", frame, slot, numUE);
}
const int min_rbSize = 5;
/* Loop UE_sched to find max coeff and allocate transmission */
while (iterator->UE != NULL) {
/* nakaolab: skip UEs already allocated as a MU-MIMO partner of an earlier UE
* in this same pf_dl call (mumimo_pf_call_id matches). */
if (mumimo_enabled() &&
iterator->UE->UE_sched_ctrl.mumimo_paired_slot == (int64_t)mumimo_pf_call_id) {
iterator++;
continue;
}
NR_UE_sched_ctrl_t *sched_ctrl = &iterator->UE->UE_sched_ctrl;
const uint16_t rnti = iterator->UE->rnti;
@@ -837,7 +904,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,
@@ -878,7 +945,179 @@ static void pf_dl(gNB_MAC_INST *mac,
&sched_pdsch.tb_size,
&sched_pdsch.rbSize);
/* nakaolab MU-MIMO: find up to MAX_MU_MIMO_PARTNERS orthogonal-PMI partner UEs
* that can ride on the same RB allocation. Each partner gets its own DCI/CCE,
* its own DMRS port (1000+layer_idx), and is forced to the leader's MCS/TBS.
* Up to 4 UEs total per slot (one per orthogonal x2 class). */
#define MAX_MU_MIMO_PARTNERS 3
NR_UE_info_t *partners[MAX_MU_MIMO_PARTNERS] = {NULL};
int partner_CCEs[MAX_MU_MIMO_PARTNERS] = {-1, -1, -1};
int partner_allocs[MAX_MU_MIMO_PARTNERS] = {-1, -1, -1};
int n_partners = 0;
/* nakaolab Phase 4: cap MU-MIMO group size to the active TX antenna count.
* When the antenna controller mutes some TX chains (e.g., 4->2 or 4->1), the
* effective number of orthogonal codewords in the codebook shrinks. Each
* co-scheduled UE consumes one spatial layer = one TX port. So:
* active_tx = 4 -> up to 4 UEs (3 partners), 4-way MU-MIMO
* active_tx = 2 -> up to 2 UEs (1 partner), 2-way MU-MIMO
* active_tx = 1 -> SU-MIMO only (0 partners) */
int max_partners_now = MAX_MU_MIMO_PARTNERS;
if (mac->antenna_state) {
const uint8_t tx_mask = nr_get_tx_antenna_bitmask(mac->antenna_state);
int active_tx = __builtin_popcount(tx_mask);
if (active_tx < 1) active_tx = 1;
if (active_tx - 1 < max_partners_now)
max_partners_now = active_tx - 1;
}
if (mumimo_enabled() && remainUEs[beam.idx] > 0 && max_partners_now > 0) {
const uint8_t leader_x2 = iterator->UE->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.pmi_x2;
uint8_t classes_used = 1 << leader_x2;
static int diag_count = 0;
bool log_diag = ((diag_count++ % 200) == 0);
if (log_diag) {
int n_cand = 0;
UE_iterator (UE_list, c) {
if (c != iterator->UE && nr_mac_ue_is_active(c) &&
c->UE_sched_ctrl.available_dl_harq.head >= 0)
n_cand++;
}
LOG_W(NR_MAC, "[mumimo-diag] %4d.%2d ENTER partner-search after RNTI %04x leader_x2=%d; eligible_others=%d\n",
frame, slot, iterator->UE->rnti, leader_x2, n_cand);
}
/* Find up to max_partners_now partners, one per remaining x2 class.
* max_partners_now is capped by active TX antenna count (Phase 4). */
while (n_partners < max_partners_now && remainUEs[beam.idx] > 0) {
NR_UE_info_t *new_partner = NULL;
int new_CCE = -1;
int new_alloc = -1;
NR_beam_alloc_t new_pbeam = {0};
UE_iterator (UE_list, cand_ue) {
if (cand_ue == iterator->UE) continue;
if (cand_ue->UE_sched_ctrl.mumimo_paired_slot == (int64_t)mumimo_pf_call_id) continue;
/* Skip if already a partner of this leader. */
bool already_partner = false;
for (int i = 0; i < n_partners; i++)
if (partners[i] == cand_ue) { already_partner = true; break; }
if (already_partner) continue;
/* Skip if cand's class is already represented (leader or earlier partner). */
const uint8_t cand_x2 = cand_ue->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.pmi_x2;
if (classes_used & (1 << cand_x2)) continue;
if (!nr_mac_ue_is_active(cand_ue)) continue;
if (cand_ue->UE_sched_ctrl.available_dl_harq.head < 0) continue;
update_dlsch_buffer(pp_pdsch->frame, pp_pdsch->slot, cand_ue);
if (!dlsch_to_schedule(&cand_ue->UE_sched_ctrl)) continue;
if (!pmi_is_orthogonal(iterator->UE, cand_ue)) continue;
NR_beam_alloc_t pbeam = beam_allocation_procedure(&mac->beam_info, frame, slot,
cand_ue->UE_beam_index,
slots_per_frame);
if (pbeam.idx != beam.idx) {
reset_beam_status(&mac->beam_info, frame, slot, cand_ue->UE_beam_index,
slots_per_frame, pbeam.new_beam);
continue;
}
int p_CCE = get_cce_index(mac, CC_id, slot, cand_ue->rnti,
&cand_ue->UE_sched_ctrl.aggregation_level,
pbeam.idx,
cand_ue->UE_sched_ctrl.search_space,
cand_ue->UE_sched_ctrl.coreset,
&cand_ue->UE_sched_ctrl.sched_pdcch,
cand_ue->UE_sched_ctrl.pdcch_cl_adjust);
if (p_CCE < 0) {
reset_beam_status(&mac->beam_info, frame, slot, cand_ue->UE_beam_index,
slots_per_frame, pbeam.new_beam);
continue;
}
int p_alloc = -1;
if (!get_FeedbackDisabled(cand_ue->sc_info.downlinkHARQ_FeedbackDisabled_r17,
cand_ue->UE_sched_ctrl.available_dl_harq.head)) {
int p_pucch = nr_get_pucch_resource(cand_ue->UE_sched_ctrl.coreset,
cand_ue->current_UL_BWP.pucch_Config, p_CCE);
p_alloc = nr_acknack_scheduling(mac, cand_ue, frame, slot,
cand_ue->UE_beam_index, p_pucch, 0);
if (p_alloc < 0) {
reset_beam_status(&mac->beam_info, frame, slot, cand_ue->UE_beam_index,
slots_per_frame, pbeam.new_beam);
continue;
}
}
new_partner = cand_ue;
new_CCE = p_CCE;
new_alloc = p_alloc;
new_pbeam = pbeam;
break;
}
if (new_partner == NULL) break;
partners[n_partners] = new_partner;
partner_CCEs[n_partners] = new_CCE;
partner_allocs[n_partners] = new_alloc;
const uint8_t p_x2 = new_partner->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.pmi_x2;
classes_used |= 1 << p_x2;
n_partners++;
(void)new_pbeam; /* beam already taken via beam_allocation_procedure */
}
if (n_partners > 0) {
/* Compose pairing log: leader + partners */
char pair_str[256];
int off = snprintf(pair_str, sizeof(pair_str), "RNTI %04x (x2=%d)",
iterator->UE->rnti, leader_x2);
for (int i = 0; i < n_partners && off < (int)sizeof(pair_str) - 32; i++)
off += snprintf(pair_str + off, sizeof(pair_str) - off, " + RNTI %04x (x2=%d)",
partners[i]->rnti,
partners[i]->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.pmi_x2);
LOG_W(NR_MAC, "[nakaolab MU-MIMO] %4d.%2d %d-way pair %s on RBs [%d..%d]\n",
frame, slot, n_partners + 1, pair_str,
sched_pdsch.rbStart, sched_pdsch.rbStart + sched_pdsch.rbSize - 1);
/* MU-MIMO needs DMRS pilots from both CDM groups protected from data. */
sched_pdsch.dmrs_parms.numDmrsCdmGrpsNoData = 2;
}
}
post_process_dlsch(mac, pp_pdsch, iterator->UE, &sched_pdsch);
if (mumimo_enabled())
iterator->UE->UE_sched_ctrl.mumimo_paired_slot = (int64_t)mumimo_pf_call_id;
/* nakaolab: allocate each MU-MIMO partner on the same RBs.
* DMRS port assignment: leader = port 1000 (mask bit 0), partner_i = port
* 1001+i (mask bit i+1). For 4-way: 1000 (CDM group 0), 1001 (CDM group 0),
* 1002 (CDM group 1), 1003 (CDM group 1). */
for (int i = 0; i < n_partners; i++) {
NR_UE_info_t *p_ue = partners[i];
NR_UE_sched_ctrl_t *p_sched_ctrl = &p_ue->UE_sched_ctrl;
NR_UE_DL_BWP_t *p_dl_bwp = &p_ue->current_DL_BWP;
p_sched_ctrl->cce_index = partner_CCEs[i];
fill_pdcch_vrb_map(mac, CC_id, &p_sched_ctrl->sched_pdcch, partner_CCEs[i],
p_sched_ctrl->aggregation_level, beam.idx);
const int p_l = 1;
NR_sched_pdsch_t p_sched_pdsch = {
.rbStart = sched_pdsch.rbStart,
.rbSize = sched_pdsch.rbSize,
.mcs = sched_pdsch.mcs,
.R = sched_pdsch.R,
.Qm = sched_pdsch.Qm,
.tb_size = sched_pdsch.tb_size,
.dl_harq_pid = p_sched_ctrl->available_dl_harq.head,
.pucch_allocation = partner_allocs[i],
.pm_index = get_pm_index(mac, p_ue, p_dl_bwp->dci_format, p_l,
mac->radio_config.pdsch_AntennaPorts.XP),
.nrOfLayers = p_l,
.bwp_info = get_pdsch_bwp_start_size(mac, p_ue),
.dmrs_parms = get_dl_dmrs_params(scc, p_dl_bwp, &tda_info, p_l),
.time_domain_allocation = tda,
.tda_info = tda_info,
.action = NULL,
};
/* Partner i (i in 0..2) uses DMRS port 1001+i: mask bit (i+1). */
p_sched_pdsch.dmrs_parms.dmrs_ports_id = 1u << (i + 1);
p_sched_pdsch.dmrs_parms.numDmrsCdmGrpsNoData = 2;
post_process_dlsch(mac, pp_pdsch, p_ue, &p_sched_pdsch);
p_sched_ctrl->mumimo_paired_slot = (int64_t)mumimo_pf_call_id;
remainUEs[beam.idx]--;
}
/* transmissions: directly allocate */
n_rb_sched[beam.idx] -= sched_pdsch.rbSize;
@@ -101,14 +101,13 @@ int get_ssbidx_from_beam(gNB_MAC_INST *mac, int beam_idx)
return 0;
}
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)
@@ -165,63 +164,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
+42 -4
View File
@@ -497,7 +497,9 @@ static void evaluate_sinr_report(NR_UE_info_t *UE,
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);
/* NULL mac arg: SINR-based MCS estimation skips antenna-state rank cap
(caller doesn't have nrmac in scope; this is only for max-MCS hint). */
const int nrOfLayers = get_dl_nrOfLayers(NULL, sched_ctrl, UE->current_DL_BWP.dci_format);
sched_ctrl->dl_max_mcs = get_mcs_from_SINRx10(mcs_table, sinr_report->r[0].SINRx10, nrOfLayers);
LOG_D(MAC, "Reported SSB-SINR = %01f, dl_max_mcs %d\n", sinr_report->r[0].SINRx10 / 10.0, sched_ctrl->dl_max_mcs);
@@ -761,8 +763,20 @@ static void extract_pucch_csi_report(NR_CSI_MeasConfig_t *csi_MeasConfig,
evaluate_cri_report(payload, cri_bitlen, cumul_bits, sched_ctrl);
cumul_bits += cri_bitlen;
ri_bitlen = csi_report->csi_meas_bitlen.ri_bitlen;
if (ri_bitlen)
if (ri_bitlen) {
r_index = evaluate_ri_report(payload, ri_bitlen, csi_report->csi_meas_bitlen.ri_restriction, cumul_bits, sched_ctrl);
} else {
/* nakaolab: ri_bitlen=0 (single RI allowed via ri_restriction).
* Pick that RI directly — without this, r_index stays -1 and trips
* the DevAssert(ri>=0) in evaluate_cqi_report. */
for (int i = 0; i < 8; i++) {
if ((csi_report->csi_meas_bitlen.ri_restriction >> i) & 0x01) {
r_index = i;
sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.ri = i;
break;
}
}
}
cumul_bits += ri_bitlen;
if (r_index != -1)
skip_zero_padding(&cumul_bits, csi_report, r_index, bitlen);
@@ -775,8 +789,20 @@ static void extract_pucch_csi_report(NR_CSI_MeasConfig_t *csi_MeasConfig,
evaluate_cri_report(payload, cri_bitlen, cumul_bits, sched_ctrl);
cumul_bits += cri_bitlen;
ri_bitlen = csi_report->csi_meas_bitlen.ri_bitlen;
if (ri_bitlen)
if (ri_bitlen) {
r_index = evaluate_ri_report(payload, ri_bitlen, csi_report->csi_meas_bitlen.ri_restriction, cumul_bits, sched_ctrl);
} else {
/* nakaolab: ri_bitlen=0 (single RI allowed via ri_restriction).
* Pick that RI directly — without this, r_index stays -1 and trips
* the DevAssert(ri>=0) in evaluate_cqi_report. */
for (int i = 0; i < 8; i++) {
if ((csi_report->csi_meas_bitlen.ri_restriction >> i) & 0x01) {
r_index = i;
sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.ri = i;
break;
}
}
}
cumul_bits += ri_bitlen;
if (r_index != -1)
skip_zero_padding(&cumul_bits, csi_report, r_index, bitlen);
@@ -792,8 +818,20 @@ static void extract_pucch_csi_report(NR_CSI_MeasConfig_t *csi_MeasConfig,
evaluate_cri_report(payload, cri_bitlen, cumul_bits, sched_ctrl);
cumul_bits += cri_bitlen;
ri_bitlen = csi_report->csi_meas_bitlen.ri_bitlen;
if (ri_bitlen)
if (ri_bitlen) {
r_index = evaluate_ri_report(payload, ri_bitlen, csi_report->csi_meas_bitlen.ri_restriction, cumul_bits, sched_ctrl);
} else {
/* nakaolab: ri_bitlen=0 (single RI allowed via ri_restriction).
* Pick that RI directly — without this, r_index stays -1 and trips
* the DevAssert(ri>=0) in evaluate_cqi_report. */
for (int i = 0; i < 8; i++) {
if ((csi_report->csi_meas_bitlen.ri_restriction >> i) & 0x01) {
r_index = i;
sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.ri = i;
break;
}
}
}
cumul_bits += ri_bitlen;
li_bitlen = evaluate_li_report(payload, csi_report, cumul_bits, r_index, sched_ctrl);
cumul_bits += li_bitlen;
+1 -1
View File
@@ -373,7 +373,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);
+9
View File
@@ -65,6 +65,7 @@
#include "LAYER2/NR_MAC_COMMON/nr_mac_common.h"
#include "LAYER2/NR_MAC_gNB/mac_config.h"
#include "NR_TAG.h"
#include "PHY/NR_TRANSPORT/nr_antenna_state.h"
/* Defs */
#define MAX_NUM_BWP 5
@@ -663,6 +664,10 @@ typedef struct {
int release_timer;
CSI_report_t CSI_report;
bool SR;
/// nakaolab MU-MIMO: encoded frame*1000+slot when this UE was last allocated as
/// a MU-MIMO partner (or leader). -1 = never. Lets pf_dl skip UEs already paired
/// in the current slot when scanning connected_ue_list for partners.
int64_t mumimo_paired_slot;
/// information about every HARQ process
NR_UE_harq_t harq_processes[NR_MAX_HARQ_PROCESSES];
/// HARQ processes that are free
@@ -965,6 +970,10 @@ typedef struct gNB_MAC_INST_s {
nr_mac_config_t radio_config;
nr_rlc_configuration_t rlc_config;
/// Pointer to PHY antenna state (port mute mask) — set in scheduler init,
/// used by get_dl_nrOfLayers to cap MIMO rank to active TX port count.
nr_antenna_state_t *antenna_state;
NR_sched_ctrl_sib1_t *sched_ctrlSIB1;
NR_sched_pdcch_t *sched_pdcch_otherSI;
uint16_t cset0_bwp_start;
+80 -11
View File
@@ -1324,7 +1324,29 @@ static void rfsimulator_read_internal(rfsimulator_state_t *t,
rxAddInput(input, temp_array[aarx], aarx, ptr->channel_model, nsamps);
}
} else {
if (is_first_beam && is_first_peer && (ptr->nbAnt == 1 || nbAnt == 1)) {
/* nakaolab MU-MIMO: when OAI_UE_CHANNEL_CLASS is set in the env, the UE
* applies a deterministic rank-1 channel whose dominant right-singular-vector
* matches Type I 4-port codebook entry w_class. This makes the UE report a
* stable PMI = class, giving the gNB scheduler natural PMI diversity across
* UEs assigned to different classes. Four classes give four mutually-orthogonal
* codewords, supporting up to 4-way MU-MIMO.
*
* Codeword w_k[a] for 4-port rank-1 (Type I, N1=2, N2=1):
* w_0 = [1, 1, 1, 1]/2
* w_1 = [1, j, -1, -j]/2
* w_2 = [1, -1, 1, -1]/2
* w_3 = [1, -j, -1, j]/2
* The 4x4 channel H^(k) is rank-1: H^(k)[r,a] = w_k[a]* (per-row constant), so
* the dominant right singular vector is w_k. */
static int ue_class_cached = -2;
if (ue_class_cached == -2) {
const char *env = getenv("OAI_UE_CHANNEL_CLASS");
ue_class_cached = (env && atoi(env) >= 0 && atoi(env) < 4) ? atoi(env) : -1;
if (ue_class_cached >= 0)
LOG_W(HW, "[nakaolab MU-MIMO] rfsim DL channel: orthogonal class %d\n", ue_class_cached);
}
if (is_first_beam && is_first_peer && (ptr->nbAnt == 1 || nbAnt == 1) && ue_class_cached < 0) {
// optimization: The buffer is uninitialized so samples can be written directly in the buffer
combine_received_beams(t, ptr->received_packets, timestamp - t->chan_offset, 1, nsamps, rx_beam_id, samples);
} else {
@@ -1334,17 +1356,64 @@ static void rfsimulator_read_internal(rfsimulator_state_t *t,
input[aatx] = ant_buffers[aatx].data();
}
combine_received_beams(t, ptr->received_packets, timestamp - t->chan_offset, ptr->nbAnt, nsamps, rx_beam_id, input);
for (int aarx = 0; aarx < nbAnt; aarx++) {
double H_awgn_mimo_coeff[ant_buffers.size()];
for (int aatx = 0; aatx < (int)ant_buffers.size(); aatx++) {
uint32_t ant_diff = std::abs(aatx - aarx);
H_awgn_mimo_coeff[aatx] = ant_diff ? (0.2 / ant_diff) : 1.0;
}
for (uint aatx = 0; aatx < ant_buffers.size(); aatx++) {
for (int i = 0; i < nsamps; i++) {
samples[aarx][i].r += ant_buffers[aatx][i].r * H_awgn_mimo_coeff[aatx];
samples[aarx][i].i += ant_buffers[aatx][i].i * H_awgn_mimo_coeff[aatx];
if (ue_class_cached >= 0 && ant_buffers.size() == 4) {
/* nakaolab MU-MIMO: per-UE channel that combines the default symmetric
* rfsim kernel with a class-specific TX-antenna weighting w_k[a]:
*
* H^(k)[r, a] = H_orig[r,a] * w_k[a]
*
* The default kernel H_orig[r,a] = (a==r ? 1.0 : 0.2/|a-r|) is full-rank,
* preserving SSB and reference-signal sync (signal reaches all RX). The
* class-specific weighting rotates each TX antenna's contribution
* differently per class, so the optimal PMI for class k is exactly
* codeword w_k from the Type I 4-port codebook (modulo diagonal scaling).
*
* w_0 = [1, 1, 1, 1]/2
* w_1 = [1, j, -1, -j]/2
* w_2 = [1, -1, 1, -1]/2
* w_3 = [1, -j, -1, j]/2 */
static const double W_RE[4][4] = {
{ 0.5, 0.5, 0.5, 0.5 },
{ 0.5, 0.0, -0.5, 0.0 },
{ 0.5, -0.5, 0.5, -0.5 },
{ 0.5, 0.0, -0.5, 0.0 }
};
static const double W_IM[4][4] = {
{ 0.0, 0.0, 0.0, 0.0 },
{ 0.0, 0.5, 0.0, -0.5 },
{ 0.0, 0.0, 0.0, 0.0 },
{ 0.0, -0.5, 0.0, 0.5 }
};
const int k = ue_class_cached;
for (int aarx = 0; aarx < nbAnt; aarx++) {
for (int aatx = 0; aatx < (int)ant_buffers.size(); aatx++) {
const uint32_t ant_diff = (uint32_t)std::abs(aatx - aarx);
const double h_orig = ant_diff ? (0.2 / ant_diff) : 1.0;
/* Effective channel coeff: h_orig * w_k[a] (complex multiply). */
const double hre = h_orig * W_RE[k][aatx];
const double him = h_orig * W_IM[k][aatx];
for (int i = 0; i < nsamps; i++) {
const double xr = ant_buffers[aatx][i].r;
const double xi = ant_buffers[aatx][i].i;
samples[aarx][i].r += (int16_t)(hre * xr - him * xi);
samples[aarx][i].i += (int16_t)(hre * xi + him * xr);
}
}
}
} else {
for (int aarx = 0; aarx < nbAnt; aarx++) {
double H_awgn_mimo_coeff[ant_buffers.size()];
for (int aatx = 0; aatx < (int)ant_buffers.size(); aatx++) {
uint32_t ant_diff = std::abs(aatx - aarx);
H_awgn_mimo_coeff[aatx] = ant_diff ? (0.2 / ant_diff) : 1.0;
}
for (uint aatx = 0; aatx < ant_buffers.size(); aatx++) {
for (int i = 0; i < nsamps; i++) {
samples[aarx][i].r += ant_buffers[aatx][i].r * H_awgn_mimo_coeff[aatx];
samples[aarx][i].i += ant_buffers[aatx][i].i * H_awgn_mimo_coeff[aatx];
}
}
}
}