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>
368 lines
12 KiB
C
368 lines
12 KiB
C
#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);
|
|
}
|