From 7cd72323af01279485082a67ab7e5fe35e50518c Mon Sep 17 00:00:00 2001 From: makigo Date: Mon, 27 Apr 2026 06:25:57 +0900 Subject: [PATCH] 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) --- CMakeLists.txt | 2 + executables/nr-gnb.c | 10 + openair1/PHY/INIT/nr_init.c | 3 + openair1/PHY/MODULATION/beamforming.c | 1 + openair1/PHY/MODULATION/nr_beamforming.c | 32 ++ openair1/PHY/MODULATION/nr_modulation.h | 27 + .../PHY/NR_TRANSPORT/nr_antenna_control.c | 532 ++++++++++++++++++ .../PHY/NR_TRANSPORT/nr_antenna_control.h | 123 ++++ openair1/PHY/NR_TRANSPORT/nr_antenna_state.c | 367 ++++++++++++ openair1/PHY/NR_TRANSPORT/nr_antenna_state.h | 239 ++++++++ openair1/PHY/defs_gNB.h | 3 + openair1/SCHED_NR/nr_ru_procedures.c | 57 +- .../LAYER2/NR_MAC_gNB/gNB_scheduler_dlsch.c | 7 +- .../NR_MAC_gNB/gNB_scheduler_primitives.c | 148 +++-- .../LAYER2/NR_MAC_gNB/gNB_scheduler_uci.c | 4 +- openair2/LAYER2/NR_MAC_gNB/mac_proto.h | 2 +- 16 files changed, 1478 insertions(+), 79 deletions(-) create mode 100644 openair1/PHY/NR_TRANSPORT/nr_antenna_control.c create mode 100644 openair1/PHY/NR_TRANSPORT/nr_antenna_control.h create mode 100644 openair1/PHY/NR_TRANSPORT/nr_antenna_state.c create mode 100644 openair1/PHY/NR_TRANSPORT/nr_antenna_state.h diff --git a/CMakeLists.txt b/CMakeLists.txt index cbc3764aba..a4a5b5dfa8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/executables/nr-gnb.c b/executables/nr-gnb.c index 1b27b2e4e6..f362afdf73 100644 --- a/executables/nr-gnb.c +++ b/executables/nr-gnb.c @@ -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"); + } } } diff --git a/openair1/PHY/INIT/nr_init.c b/openair1/PHY/INIT/nr_init.c index 35b27468f6..28565cb998 100644 --- a/openair1/PHY/INIT/nr_init.c +++ b/openair1/PHY/INIT/nr_init.c @@ -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**)); diff --git a/openair1/PHY/MODULATION/beamforming.c b/openair1/PHY/MODULATION/beamforming.c index f65a1bac48..3f117fad5a 100644 --- a/openair1/PHY/MODULATION/beamforming.c +++ b/openair1/PHY/MODULATION/beamforming.c @@ -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, diff --git a/openair1/PHY/MODULATION/nr_beamforming.c b/openair1/PHY/MODULATION/nr_beamforming.c index a10b95d6d2..584a5c0ef2 100644 --- a/openair1/PHY/MODULATION/nr_beamforming.c +++ b/openair1/PHY/MODULATION/nr_beamforming.c @@ -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); + } +} diff --git a/openair1/PHY/MODULATION/nr_modulation.h b/openair1/PHY/MODULATION/nr_modulation.h index 769bf43da1..20dc71095b 100644 --- a/openair1/PHY/MODULATION/nr_modulation.h +++ b/openair1/PHY/MODULATION/nr_modulation.h @@ -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, diff --git a/openair1/PHY/NR_TRANSPORT/nr_antenna_control.c b/openair1/PHY/NR_TRANSPORT/nr_antenna_control.c new file mode 100644 index 0000000000..d79f64ea4d --- /dev/null +++ b/openair1/PHY/NR_TRANSPORT/nr_antenna_control.c @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// 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 - Mute antenna\n" + "UNMUTE TX/RX - Unmute antenna\n" + "SET TX/RX MASK - 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 +} diff --git a/openair1/PHY/NR_TRANSPORT/nr_antenna_control.h b/openair1/PHY/NR_TRANSPORT/nr_antenna_control.h new file mode 100644 index 0000000000..bc2e061af2 --- /dev/null +++ b/openair1/PHY/NR_TRANSPORT/nr_antenna_control.h @@ -0,0 +1,123 @@ +#ifndef NR_ANTENNA_CONTROL_H +#define NR_ANTENNA_CONTROL_H + +#include "nr_antenna_state.h" +#include + +#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 - Mute TX antenna + * UNMUTE TX - Unmute TX antenna + * MUTE RX - Mute RX antenna + * UNMUTE RX - Unmute RX antenna + * SET TX MASK - Set TX antenna bitmask + * SET RX MASK - 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 */ diff --git a/openair1/PHY/NR_TRANSPORT/nr_antenna_state.c b/openair1/PHY/NR_TRANSPORT/nr_antenna_state.c new file mode 100644 index 0000000000..1c5a3df415 --- /dev/null +++ b/openair1/PHY/NR_TRANSPORT/nr_antenna_state.c @@ -0,0 +1,367 @@ +#include "nr_antenna_state.h" +#include "common/utils/LOG/log.h" +#include +#include + +// 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); +} diff --git a/openair1/PHY/NR_TRANSPORT/nr_antenna_state.h b/openair1/PHY/NR_TRANSPORT/nr_antenna_state.h new file mode 100644 index 0000000000..eb810a8e2b --- /dev/null +++ b/openair1/PHY/NR_TRANSPORT/nr_antenna_state.h @@ -0,0 +1,239 @@ +#ifndef NR_ANTENNA_STATE_H +#define NR_ANTENNA_STATE_H + +#include +#include +#include + +#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 */ + diff --git a/openair1/PHY/defs_gNB.h b/openair1/PHY/defs_gNB.h index 1ca19deda0..aa23150ace 100644 --- a/openair1/PHY/defs_gNB.h +++ b/openair1/PHY/defs_gNB.h @@ -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; diff --git a/openair1/SCHED_NR/nr_ru_procedures.c b/openair1/SCHED_NR/nr_ru_procedures.c index 68dc731f64..09cb33627b 100644 --- a/openair1/SCHED_NR/nr_ru_procedures.c +++ b/openair1/SCHED_NR/nr_ru_procedures.c @@ -21,6 +21,14 @@ #include +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); diff --git a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_dlsch.c b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_dlsch.c index d2f3b764d7..c65df50e80 100644 --- a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_dlsch.c +++ b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_dlsch.c @@ -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); @@ -700,7 +699,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, @@ -837,7 +836,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, diff --git a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_primitives.c b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_primitives.c index 3dc39f9612..d18634f661 100644 --- a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_primitives.c +++ b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_primitives.c @@ -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 diff --git a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_uci.c b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_uci.c index d0ab235276..32337fd8eb 100644 --- a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_uci.c +++ b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_uci.c @@ -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); diff --git a/openair2/LAYER2/NR_MAC_gNB/mac_proto.h b/openair2/LAYER2/NR_MAC_gNB/mac_proto.h index 5aeace199c..5481782d39 100644 --- a/openair2/LAYER2/NR_MAC_gNB/mac_proto.h +++ b/openair2/LAYER2/NR_MAC_gNB/mac_proto.h @@ -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);