From c50fe1af535ebd27a7efd279b90b951c35b88841 Mon Sep 17 00:00:00 2001 From: makigo Date: Tue, 15 Sep 2026 00:36:01 +0900 Subject: [PATCH] feat(nr): implement rank-8 PDSCH --- CMakeLists.txt | 2 + common/utils/nr/nr_common.h | 21 +- common/utils/nr/tests/test_nr_common.cpp | 16 + executables/nr-gnb.c | 10 + executables/nr-ue-ru.c | 4 +- executables/nr-ue.c | 5 +- openair1/PHY/INIT/nr_init.c | 3 + openair1/PHY/INIT/nr_init_ue.c | 10 +- openair1/PHY/MODULATION/beamforming.c | 1 + openair1/PHY/MODULATION/nr_beamforming.c | 32 + openair1/PHY/MODULATION/nr_modulation.c | 98 ++- openair1/PHY/MODULATION/nr_modulation.h | 35 +- .../MODULATION/tests/test_nr_modulation.cpp | 50 +- .../PHY/NR_TRANSPORT/nr_antenna_control.c | 651 ++++++++++++++++++ .../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/NR_TRANSPORT/nr_dlsch.c | 210 ++++-- openair1/PHY/NR_TRANSPORT/nr_dlsch_coding.c | 271 ++++---- openair1/PHY/NR_TRANSPORT/nr_sch_dmrs.h | 8 + .../nr_dl_channel_estimation.c | 3 +- openair1/PHY/NR_UE_TRANSPORT/csi_rx.c | 266 ++++++- openair1/PHY/NR_UE_TRANSPORT/dci_nr.c | 19 +- .../NR_UE_TRANSPORT/nr_dlsch_demodulation.c | 218 +++++- openair1/PHY/defs_gNB.h | 11 +- openair1/PHY/defs_nr_UE.h | 6 +- openair1/SCHED_NR/nr_ru_procedures.c | 87 ++- openair1/SCHED_NR/phy_procedures_nr_gNB.c | 11 +- openair1/SCHED_NR_UE/phy_procedures_nr_ue.c | 219 ++++-- openair1/SIMULATION/NR_PHY/dlschsim.c | 2 +- openair1/SIMULATION/NR_PHY/dlsim.c | 146 ++-- openair1/SIMULATION/TOOLS/random_channel.c | 8 + openair2/LAYER2/NR_MAC_COMMON/nr_mac_common.c | 5 +- openair2/LAYER2/NR_MAC_gNB/config.c | 11 +- .../LAYER2/NR_MAC_gNB/gNB_scheduler_dlsch.c | 326 +++++---- .../LAYER2/NR_MAC_gNB/gNB_scheduler_phytest.c | 10 +- .../NR_MAC_gNB/gNB_scheduler_primitives.c | 195 ++++-- .../LAYER2/NR_MAC_gNB/gNB_scheduler_uci.c | 82 ++- openair2/LAYER2/NR_MAC_gNB/mac_proto.h | 3 +- openair2/LAYER2/NR_MAC_gNB/nr_mac_gNB.h | 14 + openair2/LAYER2/NR_MAC_gNB/nr_radio_config.c | 50 +- radio/rfsimulator/simulator.cpp | 44 +- 42 files changed, 3244 insertions(+), 648 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..9b4efa9cfa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -978,6 +978,8 @@ set(PHY_SRC_UE ${OPENAIR1_DIR}/PHY/nr_phy_common/src/nr_phy_common_srs.c ${OPENAIR1_DIR}/PHY/NR_TRANSPORT/nr_scrambling.c ${OPENAIR1_DIR}/PHY/NR_REFSIG/refsig.c + ${OPENAIR1_DIR}/PHY/NR_TRANSPORT/nr_antenna_state.c + ${OPENAIR1_DIR}/PHY/NR_TRANSPORT/nr_antenna_control.c ) set(PHY_NR_SRC diff --git a/common/utils/nr/nr_common.h b/common/utils/nr/nr_common.h index 078da2158c..ab93256cd6 100644 --- a/common/utils/nr/nr_common.h +++ b/common/utils/nr/nr_common.h @@ -71,9 +71,28 @@ static inline const char *rnti_types(nr_rnti_type_t rr) #define MAX_GSCN_BAND 620 // n78 has the highest GSCN range of 619 #define NR_SYMBOLS_PER_SLOT 14 #define NR_SYMBOLS_PER_SLOT_EXTENDED_CP 12 -#define NR_MAX_NB_LAYERS 4 // 8 +#define NR_MAX_NB_LAYERS 8 +#define NR_MAX_NB_CODEWORDS 2 #define NR_MAX_CSI_PORTS 12 +/* TS 38.211 Table 7.3.1.3-1: PDSCH uses one codeword up to rank four and + * two codewords from rank five onward. Keep this split in one shared helper + * so MAC and PHY cannot silently disagree about the 4+4 rank-eight mapping. */ +static inline uint8_t nr_pdsch_num_codewords(uint8_t layers) +{ + AssertFatal(layers >= 1 && layers <= NR_MAX_NB_LAYERS, "Invalid PDSCH layer count %u\n", layers); + return layers > 4 ? 2 : 1; +} + +static inline uint8_t nr_pdsch_layers_per_codeword(uint8_t layers, uint8_t codeword) +{ + const uint8_t n_codewords = nr_pdsch_num_codewords(layers); + AssertFatal(codeword < n_codewords, "Invalid codeword %u for %u PDSCH layers\n", codeword, layers); + if (n_codewords == 1) + return layers; + return layers / 2 + (codeword == 1 ? layers % 2 : 0); +} + // Since the IQ samples are represented by SQ15 R+I (see https://en.wikipedia.org/wiki/Q_(number_format)) we need to compensate when // calcualting signal energy. Instead of shifting each sample right by 15, we can normalize the result in dB scale once its // calcualted. Signal energy is calculated using RMS^2, where each sample is squared before taking the average of the sum, therefore diff --git a/common/utils/nr/tests/test_nr_common.cpp b/common/utils/nr/tests/test_nr_common.cpp index 66350ff919..e971351780 100644 --- a/common/utils/nr/tests/test_nr_common.cpp +++ b/common/utils/nr/tests/test_nr_common.cpp @@ -21,6 +21,22 @@ TEST(nr_common, nr_timer) { EXPECT_TRUE(nr_timer_expired(&timer)); } +TEST(nr_common, pdsch_codeword_layer_split) +{ + constexpr uint8_t expected_codewords[] = {1, 1, 1, 1, 2, 2, 2, 2}; + constexpr uint8_t expected_cw0_layers[] = {1, 2, 3, 4, 2, 3, 3, 4}; + constexpr uint8_t expected_cw1_layers[] = {0, 0, 0, 0, 3, 3, 4, 4}; + + for (uint8_t layers = 1; layers <= NR_MAX_NB_LAYERS; layers++) { + const int index = layers - 1; + EXPECT_EQ(nr_pdsch_num_codewords(layers), expected_codewords[index]); + EXPECT_EQ(nr_pdsch_layers_per_codeword(layers, 0), expected_cw0_layers[index]); + if (expected_codewords[index] == 2) { + EXPECT_EQ(nr_pdsch_layers_per_codeword(layers, 1), expected_cw1_layers[index]); + } + } +} + int main(int argc, char **argv) { 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/executables/nr-ue-ru.c b/executables/nr-ue-ru.c index 1b0817478e..60bb3d3199 100644 --- a/executables/nr-ue-ru.c +++ b/executables/nr-ue-ru.c @@ -302,8 +302,8 @@ void nrue_init_openair0(void) cfg->ru_id = ru_id; cfg->num_rb_dl = frame_parms->N_RB_DL; - cfg->tx_num_channels = min(4, frame_parms->nb_antennas_tx); - cfg->rx_num_channels = min(4, frame_parms->nb_antennas_rx); + cfg->tx_num_channels = min(MAX_ANT, frame_parms->nb_antennas_tx); + cfg->rx_num_channels = min(MAX_ANT, frame_parms->nb_antennas_rx); LOG_I(PHY, "HW: Configuring ru_id %d, sample_rate %f, tx/rx num_channels %d/%d, duplex_mode %s\n", diff --git a/executables/nr-ue.c b/executables/nr-ue.c index 15f5040139..7d0f5e7e83 100644 --- a/executables/nr-ue.c +++ b/executables/nr-ue.c @@ -763,8 +763,9 @@ void *UE_thread(void *arg) if (get_softmodem_params()->sync_ref && UE->sl_mode == 2) { UE->is_synchronized = 1; - } else { - //warm up the RF board + } else if (!IS_SOFTMODEM_RFSIM) { + // Warm up physical RF hardware. RFsim has no RF board to settle, and + // discarding 100 simulated frames only delays initial synchronization. openair0_timestamp_t tmp; for (int i = 0; i < 50; i++) readFrame(UE, &tmp, duration_rx_to_tx, true); 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/INIT/nr_init_ue.c b/openair1/PHY/INIT/nr_init_ue.c index cccf4cf3a8..1113ad0929 100644 --- a/openair1/PHY/INIT/nr_init_ue.c +++ b/openair1/PHY/INIT/nr_init_ue.c @@ -160,8 +160,12 @@ int init_nr_ue_signal(PHY_VARS_NR_UE *ue, int nb_connected_gNB) LOG_I(PHY, "Initializing UE vars for gNB TXant %u, UE RXant %u\n", fp->nb_antennas_tx, fp->nb_antennas_rx); phy_init_nr_top(ue); - // many memory allocation sizes are hard coded - AssertFatal( fp->nb_antennas_rx <= 4, "hard coded allocation for ue_common_vars->dl_ch_estimates[gNB_id]" ); + // NR UE receive buffers are dimensioned dynamically. Keep the explicit + // platform bound so an invalid RF configuration fails before allocation. + AssertFatal(fp->nb_antennas_rx > 0 && fp->nb_antennas_rx <= MAX_ANT, + "UE RX antenna count %u exceeds platform maximum %u", + fp->nb_antennas_rx, + MAX_ANT); AssertFatal( nb_connected_gNB <= NUMBER_OF_CONNECTED_gNB_MAX, "n_connected_gNB is too large" ); // init phy_vars_ue @@ -170,7 +174,7 @@ int init_nr_ue_signal(PHY_VARS_NR_UE *ue, int nb_connected_gNB) ue->measurements.ssb_sinr_dB[i] = INT_MIN; } - for (int i = 0; i < 4; i++) { + for (int i = 0; i < fp->nb_antennas_rx; i++) { ue->rx_gain_max[i] = 135; ue->rx_gain_med[i] = 128; ue->rx_gain_byp[i] = 120; 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.c b/openair1/PHY/MODULATION/nr_modulation.c index 8a1349b648..52efcefcf4 100644 --- a/openair1/PHY/MODULATION/nr_modulation.c +++ b/openair1/PHY/MODULATION/nr_modulation.c @@ -216,15 +216,19 @@ void nr_modulation(const uint32_t *in, uint32_t length, uint16_t mod_order, int1 } void nr_layer_mapping(int nbCodes, - int encoded_len, - c16_t mod_symbs[nbCodes][encoded_len], + const c16_t *const mod_symbs[NR_MAX_NB_CODEWORDS], + const uint32_t n_symbs_per_codeword[NR_MAX_NB_CODEWORDS], uint8_t n_layers, int layerSz, - uint32_t n_symbs, c16_t tx_layers[][layerSz]) { + const uint32_t n_symbs = n_symbs_per_codeword[0]; LOG_D(PHY, "Doing layer mapping for %d layers, %d symbols\n", n_layers, n_symbs); - c16_t *mod = mod_symbs[0]; + const c16_t *mod = mod_symbs[0]; + AssertFatal(nbCodes == nr_pdsch_num_codewords(n_layers), + "Layer mapping got %d codewords for %u layers\n", + nbCodes, + n_layers); switch (n_layers) { case 1: memcpy(tx_layers[0], mod, n_symbs * sizeof(**mod_symbs)); @@ -472,58 +476,26 @@ void nr_layer_mapping(int nbCodes, case 5: case 6: case 7: - case 8: - /* - // Layer 0,1 - for (int i = 0; i < n_symbs; i += 2) { -const int txIdx = i / 2; -tx_layer[0][txIdx] = mod_symbs[0][i]; -tx_layer[1][txIdx] = mod_symbs[0][i + 1]; + case 8: { + int first_layer = 0; + for (int q = 0; q < nbCodes; q++) { + const int cw_layers = nr_pdsch_layers_per_codeword(n_layers, q); + AssertFatal(n_symbs_per_codeword[q] % cw_layers == 0, + "Codeword %d has %u symbols, not divisible by %d layers\n", + q, + n_symbs_per_codeword[q], + cw_layers); + const uint32_t symbols_per_layer = n_symbs_per_codeword[q] / cw_layers; + AssertFatal(symbols_per_layer <= layerSz, + "Layer buffer too small: need %u, have %d symbols\n", + symbols_per_layer, + layerSz); + for (uint32_t i = 0; i < symbols_per_layer; i++) + for (int l = 0; l < cw_layers; l++) + tx_layers[first_layer + l][i] = mod_symbs[q][i * cw_layers + l]; + first_layer += cw_layers; } - // layers 2,3,4 - else -for (int i = 0; i < n_symbs; i += 3) { -const int txIdx = i / 3; -tx_layer[2][txIdx] = mod_symbs[1][i + 2]; -tx_layer[3][txIdx] = mod_symbs[1][i + 3]; -tx_layer[4][txIdx] = mod_symbs[1][i + 4]; -} - break; - -case 6: - for (int q=0; q<2; q++) -for (int i = 0; i < n_symbs; i += 3) { -const int txIdx = i / 3; -tx_layer[0][txIdx] = mod_symbs[q][i + layer]; -tx_layer[1][txIdx] = mod_symbs[q][i + layer]; -tx_layer[2][txIdx] = mod_symbs[q][i + layer]; -tx_layer[3][txIdx] = mod_symbs[q][i + layer]; -tx_layer[4][txIdx] = mod_symbs[q][i + layer]; -tx_layer[5][txIdx] = mod_symbs[q][i + layer]; -} - break; - -case 7: - if (layer < 3) -for (int i = 0; i < n_symbs; i += 3) { -const int txIdx = i / 3; -tx_layer[txIdx] = mod_symbs[1][i + layer]; -} - else -for (int i = 0; i < n_symbs; i += 4) { -const int txIdx = i / 4; -tx_layer[txIdx] = mod_symbs[0][i + layer]; -} - break; - -case 8: - for (int q=0; q<2; q++) - for (int i = 0; i < n_symbs; i += 4) { -const int txIdx = i / 4; -tx_layer[txIdx] = mod_symbs[q][i + layer]; - } - break; -*/ + } break; default: AssertFatal(0, "Invalid number of layers %d\n", n_layers); } @@ -687,7 +659,7 @@ c16_t nr_layer_precoder(int sz, c16_t datatx_F_precoding[][sz], const char *prec c16_t nr_layer_precoder_cm(int n_layers, int symSz, - c16_t datatx_F_precoding[n_layers][symSz], + const c16_t datatx_F_precoding[n_layers][symSz], int ap, c16_t weights[NR_MAX_NB_LAYERS][NR_MAX_CSI_PORTS], int offset) @@ -819,7 +791,19 @@ void nr_layer_precoder_simd(const int n_layers, { // For x86, use 256 SIMD for every 8 RE and 128 SIMD for last 4 RE // For aarch64, use 128 SIMD for every 4 RE - AssertFatal(n_layers > 0 && n_layers <= 4, "Shouldn't get here, n_layers %d\n", n_layers); + AssertFatal(n_layers > 0 && n_layers <= NR_MAX_NB_LAYERS, "Invalid PDSCH layer count %d\n", n_layers); + + /* The hand-vectorized kernels below remain the fast path for ranks 1--4. + * Rank 5--8 is uncommon and uses the same full complex matrix operation via + * the scalar reference kernel. Unlike antenna fan-out, this combines every + * distinct layer with its own precoder weight and is therefore a genuine + * high-rank precoder. */ + if (n_layers > 4) { + for (int re = 0; re < re_cnt; re++) + txdataF_precoded[sc_offset + re] = + nr_layer_precoder_cm(n_layers, symSz, txdataF_res_mapped, ant, weights, sc_offset + re); + return; + } // 512/256 SIMD: Do 16/8 RE in one iteration, 3 iterations for 2 RB c16_t *beginning = txdataF_precoded + sc_offset; diff --git a/openair1/PHY/MODULATION/nr_modulation.h b/openair1/PHY/MODULATION/nr_modulation.h index 769bf43da1..8def687822 100644 --- a/openair1/PHY/MODULATION/nr_modulation.h +++ b/openair1/PHY/MODULATION/nr_modulation.h @@ -7,6 +7,7 @@ #include #include "PHY/defs_nr_common.h" +#include "PHY/NR_TRANSPORT/nr_antenna_state.h" #define DMRS_MOD_ORDER 2 /*Precoding matices: W[pmi][antenna_port][layer]*/ @@ -36,11 +37,10 @@ void nr_modulation(const uint32_t *in, */ void nr_layer_mapping(int nbCodes, - int encoded_len, - c16_t mod_symbs[nbCodes][encoded_len], + const c16_t *const mod_symbs[NR_MAX_NB_CODEWORDS], + const uint32_t n_symbs_per_codeword[NR_MAX_NB_CODEWORDS], uint8_t n_layers, int layerSz, - uint32_t n_symbs, c16_t tx_layers[][layerSz]); /*! \brief Perform NR layer mapping. TS 38.211 V15.4.0 subclause 7.3.1.3 @@ -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, @@ -124,7 +151,7 @@ c16_t nr_layer_precoder(int sz, c16_t datatx_F_precoding[][sz], const char *prec c16_t nr_layer_precoder_cm(int n_layers, int symSz, - c16_t datatx_F_precoding[n_layers][symSz], + const c16_t datatx_F_precoding[n_layers][symSz], int ap, c16_t weights[NR_MAX_NB_LAYERS][NR_MAX_CSI_PORTS], int offset); diff --git a/openair1/PHY/MODULATION/tests/test_nr_modulation.cpp b/openair1/PHY/MODULATION/tests/test_nr_modulation.cpp index 7a66696384..0cde3c3826 100644 --- a/openair1/PHY/MODULATION/tests/test_nr_modulation.cpp +++ b/openair1/PHY/MODULATION/tests/test_nr_modulation.cpp @@ -16,13 +16,14 @@ extern "C" { #include "openair1/PHY/TOOLS/tools_defs.h" // c16_t #include "nfapi/open-nFAPI/nfapi/public_inc/fapi_nr_ue_interface.h" // fapi_nr_dl_config_dci_dl_pdu_rel15_t +#include "common/utils/LOG/log.h" //#include "openair1/PHY/MODULATION/nr_modulation.h" // nr_layer_precoder_simd_[cm, simd] // Forward declare the function but hide the Variable-Length Array (VLA) feature from C not available in C++ c16_t nr_layer_precoder_cm(int n_layers, int symbol_size, - c16_t (*dataF_in)[/*symbol_size*/], // c16_t dataF_in[n_layers][symbol_size] + const c16_t (*dataF_in)[/*symbol_size*/], // c16_t dataF_in[n_layers][symbol_size] int ant, c16_t weights[NR_MAX_NB_LAYERS][NR_MAX_CSI_PORTS], int offset); @@ -37,6 +38,14 @@ void nr_layer_precoder_simd(int n_layers, int re_cnt, c16_t *dataF_out); +// Forward declare the layer mapper while hiding its C VLA dimension. +void nr_layer_mapping(int nb_codewords, + const c16_t *const mod_symbols[2], + const uint32_t n_symbols_per_codeword[2], + uint8_t n_layers, + int layer_size, + c16_t (*tx_layers)[/*layer_size*/]); + #ifdef __cplusplus } #endif @@ -185,11 +194,50 @@ TEST(NrLayerPrecoderTest, Compare_CM_SIMD) } } +TEST(NrLayerMappingTest, Rank8UsesTwoCodewordsAcrossEightDistinctLayers) +{ + constexpr int n_codewords = 2; + constexpr int n_layers = 8; + constexpr int layers_per_codeword = 4; + constexpr int symbols_per_layer = 7; + constexpr int symbols_per_codeword = layers_per_codeword * symbols_per_layer; + + std::vector cw0(symbols_per_codeword); + std::vector cw1(symbols_per_codeword); + for (int i = 0; i < symbols_per_codeword; ++i) { + cw0[i] = {static_cast(100 + i), static_cast(-100 - i)}; + cw1[i] = {static_cast(1000 + i), static_cast(-1000 - i)}; + } + + const c16_t *mod_symbols[2] = {cw0.data(), cw1.data()}; + const uint32_t n_symbols_per_codeword[2] = {symbols_per_codeword, symbols_per_codeword}; + std::vector layer_storage(n_layers * symbols_per_layer, c16_t{SHRT_MIN, SHRT_MIN}); + c16_t(*tx_layers)[symbols_per_layer] = reinterpret_cast(layer_storage.data()); + + nr_layer_mapping(n_codewords, + mod_symbols, + n_symbols_per_codeword, + n_layers, + symbols_per_layer, + tx_layers); + + for (int re = 0; re < symbols_per_layer; ++re) { + for (int layer = 0; layer < n_layers; ++layer) { + const int cw = layer / layers_per_codeword; + const int cw_layer = layer % layers_per_codeword; + const c16_t expected = mod_symbols[cw][re * layers_per_codeword + cw_layer]; + EXPECT_EQ(tx_layers[layer][re].r, expected.r) << "real mismatch at layer " << layer << ", RE " << re; + EXPECT_EQ(tx_layers[layer][re].i, expected.i) << "imag mismatch at layer " << layer << ", RE " << re; + } + } +} + int main(int argc, char **argv) { // Initialize random seed srand(time(0)); + logInit(); testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } 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..fc9fa02e98 --- /dev/null +++ b/openair1/PHY/NR_TRANSPORT/nr_antenna_control.c @@ -0,0 +1,651 @@ +/* + * 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 +#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; + int64_t tx_lock_until_ms[NR_MAX_ANTENNAS]; +#ifdef OAI_MPLANE + ru_session_t *ru_session; +#else + void *ru_session; +#endif +} g_ctrl = {0}; + +static bool valid_tsm_duration(int seconds) +{ + return seconds == 0 || seconds == 15 || seconds == 30 || + seconds == 45 || seconds == 60; +} + +static int64_t monotonic_ms(void) +{ + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + return (int64_t)now.tv_sec * 1000 + now.tv_nsec / 1000000; +} + +static int64_t tx_lock_remaining_ms(int ant_id) +{ + if (ant_id < 0 || ant_id >= NR_MAX_ANTENNAS) + return 0; + int64_t remaining = g_ctrl.tx_lock_until_ms[ant_id] - monotonic_ms(); + if (remaining <= 0) { + g_ctrl.tx_lock_until_ms[ant_id] = 0; + return 0; + } + return remaining; +} + +static void set_tx_lock(int ant_id, int seconds) +{ + if (ant_id < 0 || ant_id >= NR_MAX_ANTENNAS) + return; + g_ctrl.tx_lock_until_ms[ant_id] = + seconds > 0 ? monotonic_ms() + (int64_t)seconds * 1000 : 0; +} + +static void clear_all_tx_locks(void) +{ + memset(g_ctrl.tx_lock_until_ms, 0, sizeof(g_ctrl.tx_lock_until_ms)); +} + +// ============================================================================ +// 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 = -1; + int duration = 0; + int fields = sscanf(p + 8, "%d %d", &ant_id, &duration); + if (fields < 1 || ant_id < 0 || ant_id >= g_ctrl.ant_state->num_tx_antennas) { + snprintf(response, resp_size, "ERROR: Invalid TX antenna id\n"); + return; + } + if (fields >= 2 && !valid_tsm_duration(duration)) { + snprintf(response, resp_size, + "ERROR: Unsupported TSM duration %d; use 15, 30, 45, or 60 seconds\n", + duration); + return; + } + nr_mute_tx_antenna(g_ctrl.ant_state, ant_id); + set_tx_lock(ant_id, duration); + snprintf(response, resp_size, "OK: TX antenna %d muted (TSM=%ds)\n", + ant_id, duration); + } + else if (strncmp(p, "UNMUTE TX ", 10) == 0) { + int ant_id = -1; + char option[16] = {0}; + int fields = sscanf(p + 10, "%d %15s", &ant_id, option); + if (fields < 1 || ant_id < 0 || ant_id >= g_ctrl.ant_state->num_tx_antennas) { + snprintf(response, resp_size, "ERROR: Invalid TX antenna id\n"); + return; + } + bool force = fields >= 2 && strcmp(option, "FORCE") == 0; + int64_t remaining = tx_lock_remaining_ms(ant_id); + if (remaining > 0 && !force) { + snprintf(response, resp_size, + "ERROR: TX antenna %d locked for %ld more ms\n", + ant_id, (long)remaining); + return; + } + set_tx_lock(ant_id, 0); + nr_unmute_tx_antenna(g_ctrl.ant_state, ant_id); + snprintf(response, resp_size, "OK: TX antenna %d unmuted%s\n", + ant_id, force ? " (emergency override)" : ""); + } + 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; + int duration = 0; + char option[16] = {0}; + int fields = sscanf(p + 12, "%x %d %15s", &mask, &duration, option); + if (fields < 1) { + snprintf(response, resp_size, "ERROR: Invalid mask format. Use hex (e.g., 0F or 0x0F)\n"); + return; + } + if (fields >= 2 && !valid_tsm_duration(duration)) { + snprintf(response, resp_size, + "ERROR: Unsupported TSM duration %d; use 15, 30, 45, or 60 seconds\n", + duration); + return; + } + bool force = fields >= 3 && strcmp(option, "FORCE") == 0; + uint8_t valid_mask = (1u << g_ctrl.ant_state->num_tx_antennas) - 1u; + uint8_t requested = (uint8_t)mask & valid_mask; + uint8_t old = nr_get_tx_antenna_bitmask(g_ctrl.ant_state); + uint8_t waking = requested & ~old; + for (int ant_id = 0; ant_id < g_ctrl.ant_state->num_tx_antennas; ++ant_id) { + if ((waking & (1u << ant_id)) && tx_lock_remaining_ms(ant_id) > 0 && !force) { + snprintf(response, resp_size, + "ERROR: TX antenna %d is still TSM-locked\n", ant_id); + return; + } + } + + nr_set_tx_antenna_bitmask(g_ctrl.ant_state, requested); + uint8_t muted = old & ~requested; + for (int ant_id = 0; ant_id < g_ctrl.ant_state->num_tx_antennas; ++ant_id) { + uint8_t bit = 1u << ant_id; + if (muted & bit) + set_tx_lock(ant_id, duration); + if (waking & bit) + set_tx_lock(ant_id, 0); + } + snprintf(response, resp_size, + "OK: TX mask 0x%02X -> 0x%02X (TSM=%ds%s)\n", + old, requested, duration, force ? ", emergency override" : ""); + } + 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"); + size_t used = strlen(response); + if (used < resp_size) { + used += snprintf(response + used, resp_size - used, "TSM locks:"); + for (int ant_id = 0; + ant_id < g_ctrl.ant_state->num_tx_antennas && used < resp_size; + ++ant_id) { + int64_t remaining = tx_lock_remaining_ms(ant_id); + if (remaining > 0) + used += snprintf(response + used, resp_size - used, + " TX%d=%lds", ant_id, (long)((remaining + 999) / 1000)); + } + if (used < resp_size) + snprintf(response + used, resp_size - used, "\n"); + } + } + 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 + clear_all_tx_locks(); + 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 [15|30|45|60] - Mute TX antenna with optional TSM lock\n" + "UNMUTE TX [FORCE] - Unmute TX antenna\n" + "MUTE/UNMUTE RX - Change RX antenna state\n" + "SET TX MASK [duration] [FORCE] - Set TX mask and TSM lock\n" + "SET RX MASK - Set RX 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..f242bbcdb9 --- /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 [15|30|45|60] - Mute TX antenna with optional TSM lock + * UNMUTE TX [FORCE] - Unmute TX antenna; FORCE breaks lock + * MUTE RX - Mute RX antenna + * UNMUTE RX - Unmute RX antenna + * SET TX MASK [duration] [FORCE] - Set TX mask and per-chain TSM lock + * 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/NR_TRANSPORT/nr_dlsch.c b/openair1/PHY/NR_TRANSPORT/nr_dlsch.c index 7edf19dec5..883883d9d5 100644 --- a/openair1/PHY/NR_TRANSPORT/nr_dlsch.c +++ b/openair1/PHY/NR_TRANSPORT/nr_dlsch.c @@ -17,9 +17,55 @@ #include "executables/softmodem-common.h" #include "SCHED_NR/sched_nr.h" +#include +#include +#include #include #define USE128BIT +static pthread_once_t tx_layer_fanout_once = PTHREAD_ONCE_INIT; +static bool tx_layer_fanout = false; + +static void configure_tx_layer_fanout(void) +{ + const char *value = getenv("OAI_TX_LAYER_FANOUT"); + tx_layer_fanout = value != NULL && strcmp(value, "1") == 0; + if (tx_layer_fanout) + LOG_I(PHY, + "[8TX_FANOUT] Fixed identity-layer fan-out enabled; " + "all active physical TX ports carry PDSCH\n"); +} + +static bool tx_layer_fanout_enabled(void) +{ + pthread_once(&tx_layer_fanout_once, configure_tx_layer_fanout); + return tx_layer_fanout; +} + +static void copy_normalized_layer(c16_t *dst, + const c16_t *src, + int count, + int replicas) +{ + // Q15 approximations of 1/sqrt(n), n=1..8. Normalizing each duplicated + // layer keeps total frequency-domain energy comparable with the 4TX + // identity mapping instead of giving 8TX an artificial power advantage. + static const int16_t gain_q15[] = { + 32767, 23170, 18919, 16384, 14654, 13377, 12384, 11585}; + AssertFatal(replicas >= 1 && replicas <= 8, + "Invalid 8TX layer replica count %d\n", + replicas); + if (replicas == 1) { + memcpy(dst, src, count * sizeof(*dst)); + return; + } + const int32_t gain = gain_q15[replicas - 1]; + for (int i = 0; i < count; ++i) { + dst[i].r = ((int32_t)src[i].r * gain) >> 15; + dst[i].i = ((int32_t)src[i].i * gain) >> 15; + } +} + static void nr_pdsch_codeword_scrambling(uint8_t *in, uint32_t size, uint8_t q, uint32_t Nid, uint32_t n_RNTI, uint32_t *out) { nr_codeword_scrambling(in, size, q, Nid, n_RNTI, out); @@ -436,6 +482,49 @@ static inline void do_txdataF(c16_t **txdataF, int txdataF_offset_per_symbol) { NR_DL_FRAME_PARMS *frame_parms = &gNB->frame_parms; + // nakaolab Patch 8: identity-precoding layer-to-antenna remapping. + // When antennas are muted, layer N must NOT be sent to muted antenna N + // (the layer's data would be lost on the blanked port). Instead, find + // this antenna's rank among active antennas — that's the layer index it + // should receive. Muted antennas get zeros. + const nr_antenna_state_t *ast = &gNB->antenna_state; + int identity_layer; + int identity_replicas = 1; + int active_count = nr_get_active_tx_antenna_count(ast); + const bool fanout = frame_parms->nb_antennas_tx == 8 + && tx_layer_fanout_enabled(); + if (active_count <= 0 || !nr_is_tx_antenna_active(ast, ant)) { + identity_layer = -1; + } else if (fanout) { + // If physical TX ports outnumber the scheduled layers, map active ports + // cyclically and normalize replicas (for example, 8TX/4 layers). At + // rank 8 this naturally becomes a one-to-one mapping: every port carries + // distinct layer data and no fan-out occurs. + int active_rank = 0; + for (int a = 0; a < ant; ++a) + if (nr_is_tx_antenna_active(ast, a)) + active_rank++; + identity_layer = rel15->nrOfLayers > 0 + ? active_rank % rel15->nrOfLayers + : -1; + if (identity_layer >= 0) + identity_replicas = (active_count + rel15->nrOfLayers - 1 + - identity_layer) / rel15->nrOfLayers; + } else if (active_count < frame_parms->nb_antennas_tx) { + if (!nr_is_tx_antenna_active(ast, ant)) { + identity_layer = -1; // muted antenna → zero output + } else { + // count active antennas with index < ant + identity_layer = 0; + for (int a = 0; a < ant; ++a) + if (nr_is_tx_antenna_active(ast, a)) + identity_layer++; + if (identity_layer >= rel15->nrOfLayers) + identity_layer = -1; + } + } else { + identity_layer = (ant < rel15->nrOfLayers) ? ant : -1; + } int rb = 0; uint16_t subCarrier = get_block_start_sc(rb_start, rel15->BWPStart, symbol_sz); const nfapi_nr_tx_precoding_and_beamforming_t *pb = &rel15->precodingAndBeamforming; @@ -452,10 +541,13 @@ static inline void do_txdataF(c16_t **txdataF, const int rb_step = rb_step0 == 2 && pmi3 == pmi && pmi4 == pmi ? 4 : rb_step0; const int re_cnt = NR_NB_SC_PER_RB * rb_step; if (pmi == 0) { // unitary Precoding - if (ant < rel15->nrOfLayers) - memcpy(&txdataF[ant][txdataF_offset_per_symbol + subCarrier], - &txdataF_precoding[ant][subCarrier], - re_cnt * sizeof(**txdataF)); + if (identity_layer >= 0) { + copy_normalized_layer( + &txdataF[ant][txdataF_offset_per_symbol + subCarrier], + &txdataF_precoding[identity_layer][subCarrier], + re_cnt, + fanout ? identity_replicas : 1); + } else memset(&txdataF[ant][txdataF_offset_per_symbol + subCarrier], 0, re_cnt * sizeof(**txdataF)); subCarrier += re_cnt; @@ -501,7 +593,7 @@ typedef struct pdschSymbolProc_s { unsigned int n_ptrs; unsigned int beam_nb; unsigned int re_beginning_of_symbol[14]; - c16_t *tx_layers[4]; + c16_t *tx_layers[NR_MAX_NB_LAYERS]; time_stats_t dlsch_resource_mapping_stats; time_stats_t dlsch_precoding_stats; } pdschSymbolProc_t; @@ -528,20 +620,10 @@ static void nr_pdsch_symbol_processing(void *arg) for (int l_symbol = rdata->startSymbol; l_symbol < rdata->startSymbol + rdata->numSymbols; l_symbol++) { start_meas(&rdata->dlsch_resource_mapping_stats); - int l_prime = 0; // single symbol layer 0 - int l_overline = get_l0(rel15->dlDmrsSymbPos); + const int l_prime = get_pdsch_dmrs_l_prime(rel15->dlDmrsSymbPos, l_symbol); /// DMRS QPSK modulation if ((rel15->dlDmrsSymbPos & (1 << l_symbol))) { // DMRS time occasion - // The reference point for is subcarrier -1 of the lowest-numbered resource block in CORESET 0 if the corresponding - // PDCCH is associated with CORESET -1 and Type0-PDCCH common search space and is addressed to SI-RNTI - // 2GPP TS 38.211 V15.8.0 Section 7.4.1.1.2 Mapping to physical resources - if (l_symbol == (l_overline + 1)) // take into account the double DMRS symbols - l_prime = 1; - else if (l_symbol > (l_overline + 1)) { // new DMRS pair - l_overline = l_symbol; - l_prime = 0; - } const uint32_t *gold = nr_gold_pdsch(frame_parms->N_RB_DL, frame_parms->symbols_per_slot, rel15->dlDmrsScramblingId, @@ -612,12 +694,11 @@ static int do_one_dlsch(unsigned char *input_ptr, PHY_VARS_gNB *gNB, NR_gNB_DLSC const int layerSz = frame_parms->N_RB_DL * frame_parms->symbols_per_slot * NR_NB_SC_PER_RB; const int nb_re_dmrs = rel15->numDmrsCdmGrpsNoData * (rel15->dmrsConfigType == NFAPI_NR_DMRS_TYPE1 ? 6 : 4); const int n_dmrs = freq_alloc->num_rbs * nb_re_dmrs; - - const int xOverhead = 0; - const int nb_re = - (12 * rel15->NrOfSymbols - nb_re_dmrs * get_num_dmrs(rel15->dlDmrsSymbPos) - xOverhead) * freq_alloc->num_rbs * rel15->nrOfLayers; - const int Qm = rel15->qamModOrder[0]; - const int encoded_length = nb_re * Qm; + uint32_t encoded_length[NR_MAX_NB_CODEWORDS] = {0}; + uint32_t n_mod_symbols[NR_MAX_NB_CODEWORDS] = {0}; + const c16_t *mod_symbs[NR_MAX_NB_CODEWORDS] = {0}; + c16_t *mod_symbs_storage[NR_MAX_NB_CODEWORDS] = {0}; + size_t encoded_offset = 0; /* PTRS */ uint16_t dlPtrsSymPos = 0; @@ -633,34 +714,51 @@ static int do_one_dlsch(unsigned char *input_ptr, PHY_VARS_gNB *gNB, NR_gNB_DLSC #ifdef DEBUG_DLSCH printf("PDSCH encoding:\nPayload:\n"); - for (int i = 0; i < (dlsch->B >> 3); i += 16) { - for (int j = 0; j < 16; j++) - printf("0x%02x\t", dlsch->pdu[i + j]); - printf("\n"); - } - printf("\nEncoded payload:\n"); - for (int i = 0; i < encoded_length; i += 8) { - for (int j = 0; j < 8; j++) - printf("%d", (input_ptr[i >> 3] >> j) & 1); - printf("\t"); + for (int q = 0; q < rel15->NrOfCodewords; q++) { + printf("codeword %d:\n", q); + for (int i = 0; i < rel15->TBSize[q]; i++) + printf("0x%02x%s", dlsch->pdu[q][i], (i & 15) == 15 ? "\n" : "\t"); } printf("\n"); #endif - if (IS_SOFTMODEM_DLSIM) - memcpy(dlsch->f, input_ptr, (encoded_length + 7) >> 3); - - c16_t mod_symbs[rel15->NrOfCodewords][encoded_length] __attribute__((aligned(64))); for (int codeWord = 0; codeWord < rel15->NrOfCodewords; codeWord++) { + const int cw_layers = nr_pdsch_layers_per_codeword(rel15->nrOfLayers, codeWord); + const int Qm = rel15->qamModOrder[codeWord]; + encoded_length[codeWord] = nr_get_G(freq_alloc->num_rbs, + rel15->NrOfSymbols, + nb_re_dmrs, + get_num_dmrs(rel15->dlDmrsSymbPos), + dlsch->unav_res, + Qm, + cw_layers); + n_mod_symbols[codeWord] = encoded_length[codeWord] / Qm; + unsigned char *codeword_input = input_ptr + encoded_offset; + const size_t max_encoded_bits = + freq_alloc->num_rbs * frame_parms->symbols_per_slot * NR_NB_SC_PER_RB * Qm * cw_layers; + encoded_offset += ceil_mod(max_encoded_bits, 8 * 64) >> 3; + + if (IS_SOFTMODEM_DLSIM) + memcpy(dlsch->f[codeWord], codeword_input, (encoded_length[codeWord] + 7) >> 3); + + mod_symbs_storage[codeWord] = malloc16(n_mod_symbols[codeWord] * sizeof(c16_t)); + AssertFatal(mod_symbs_storage[codeWord], "Cannot allocate codeword %d modulation buffer\n", codeWord); + mod_symbs[codeWord] = mod_symbs_storage[codeWord]; + /// scrambling start_meas(dlsch_scrambling_stats); - uint32_t scrambled_output[(encoded_length >> 5) + 4]; // modulator acces by 4 bytes in some cases + uint32_t scrambled_output[(encoded_length[codeWord] >> 5) + 4]; // modulator accesses four-byte groups memset(scrambled_output, 0, sizeof(scrambled_output)); - nr_pdsch_codeword_scrambling(input_ptr, encoded_length, codeWord, rel15->dataScramblingId, rel15->rnti, scrambled_output); + nr_pdsch_codeword_scrambling(codeword_input, + encoded_length[codeWord], + codeWord, + rel15->dataScramblingId, + rel15->rnti, + scrambled_output); #ifdef DEBUG_DLSCH printf("PDSCH scrambling:\n"); - for (int i = 0; i < encoded_length >> 8; i++) { + for (int i = 0; i < encoded_length[codeWord] >> 8; i++) { for (int j = 0; j < 8; j++) printf("0x%08x\t", scrambled_output[(i << 3) + j]); printf("\n"); @@ -670,16 +768,10 @@ static int do_one_dlsch(unsigned char *input_ptr, PHY_VARS_gNB *gNB, NR_gNB_DLSC stop_meas(dlsch_scrambling_stats); /// Modulation start_meas(dlsch_modulation_stats); - nr_modulation(scrambled_output, encoded_length, Qm, (int16_t *)mod_symbs[codeWord]); + nr_modulation(scrambled_output, encoded_length[codeWord], Qm, (int16_t *)mod_symbs_storage[codeWord]); stop_meas(dlsch_modulation_stats); #ifdef DEBUG_DLSCH - printf("PDSCH Modulation: Qm %d(%d)\n", Qm, nb_re); - for (int i = 0; i < nb_re; i += 8) { - for (int j = 0; j < 8; j++) { - printf("%d %d\t", mod_symbs[codeWord][i + j].r, mod_symbs[codeWord][i + j].i); - } - printf("\n"); - } + printf("PDSCH codeword %d modulation: Qm %d, %u symbols\n", codeWord, Qm, n_mod_symbols[codeWord]); #endif } @@ -694,7 +786,7 @@ static int do_one_dlsch(unsigned char *input_ptr, PHY_VARS_gNB *gNB, NR_gNB_DLSC int layerSz2 = (layerSz + 63) & ~63; c16_t tx_layers[rel15->nrOfLayers][layerSz2] __attribute__((aligned(64))); memset(tx_layers, 0, sizeof(tx_layers)); - nr_layer_mapping(rel15->NrOfCodewords, encoded_length, mod_symbs, rel15->nrOfLayers, layerSz2, nb_re, tx_layers); + nr_layer_mapping(rel15->NrOfCodewords, mod_symbs, n_mod_symbols, rel15->nrOfLayers, layerSz2, tx_layers); /// Layer Precoding and Antenna port mapping // tx_layers 1-8 are mapped on antenna ports 1000-1007 @@ -777,12 +869,13 @@ static int do_one_dlsch(unsigned char *input_ptr, PHY_VARS_gNB *gNB, NR_gNB_DLSC merge_meas(&gNB->dlsch_resource_mapping_stats, &arr[i].dlsch_resource_mapping_stats); merge_meas(&gNB->dlsch_precoding_stats, &arr[i].dlsch_precoding_stats); } + for (int q = 0; q < rel15->NrOfCodewords; q++) + free16(mod_symbs_storage[q], n_mod_symbols[q] * sizeof(c16_t)); stop_meas(&gNB->dlsch_pdsch_generation_stats); /* output and its parts for each dlsch should be aligned on 64 bytes (or 8 * 64 bits) * should remain a multiple of 8 * 64 with enough offset to fit each dlsch */ - uint32_t size_output_tb = freq_alloc->num_rbs * frame_parms->symbols_per_slot * NR_NB_SC_PER_RB * Qm * rel15->nrOfLayers; - return ((size_output_tb + 511) >> 9) << 6; + return encoded_offset; } void nr_generate_pdsch(PHY_VARS_gNB *gNB, int n_dlsch, NR_gNB_DLSCH_t *dlsch_array, int frame, int slot) @@ -816,8 +909,6 @@ void nr_generate_pdsch(PHY_VARS_gNB *gNB, int n_dlsch, NR_gNB_DLSCH_t *dlsch_arr dlsch->freq_alloc.last_rb, dlsch->freq_alloc.num_rbs); - const int Qm = rel15->qamModOrder[0]; - /* PTRS */ uint16_t dlPtrsSymPos = 0; int n_ptrs = 0; @@ -834,13 +925,14 @@ void nr_generate_pdsch(PHY_VARS_gNB *gNB, int n_dlsch, NR_gNB_DLSCH_t *dlsch_arr dlsch->unav_res = ptrsSymbPerSlot * n_ptrs; /// CRC, coding, interleaving and rate matching - AssertFatal(dlsch->pdu != NULL, "%4d.%2d no PDU for PDSCH generation\n", frame, slot); - - /* output and its parts for each dlsch should be aligned on 64 bytes (or 8 * 64 bits) - * => size_output is a sum of parts sizes rounded up to a multiple of 8 * 64 - */ - size_t size_output_tb = dlsch->freq_alloc.num_rbs * NR_SYMBOLS_PER_SLOT * NR_NB_SC_PER_RB * Qm * rel15->nrOfLayers; - size_output += ceil_mod(size_output_tb, 8 * 64); + for (int q = 0; q < rel15->NrOfCodewords; q++) { + AssertFatal(dlsch->pdu[q] != NULL, "%4d.%2d no PDU for PDSCH codeword %d generation\n", frame, slot, q); + /* Every LDPC output starts at a 64-byte boundary. */ + const int cw_layers = nr_pdsch_layers_per_codeword(rel15->nrOfLayers, q); + const size_t size_output_tb = + dlsch->freq_alloc.num_rbs * NR_SYMBOLS_PER_SLOT * NR_NB_SC_PER_RB * rel15->qamModOrder[q] * cw_layers; + size_output += ceil_mod(size_output_tb, 8 * 64); + } } unsigned char output[size_output >> 3] __attribute__((aligned(64))); diff --git a/openair1/PHY/NR_TRANSPORT/nr_dlsch_coding.c b/openair1/PHY/NR_TRANSPORT/nr_dlsch_coding.c index bfe912dac8..b1e6ff474a 100644 --- a/openair1/PHY/NR_TRANSPORT/nr_dlsch_coding.c +++ b/openair1/PHY/NR_TRANSPORT/nr_dlsch_coding.c @@ -23,35 +23,53 @@ // #define DEBUG_DLSCH_CODING // #define DEBUG_DLSCH_FREE 1 +/* Keep the PHY encoder independent of the MAC helper with the same rule from + * TS 38.212 section 5.3.2. A dual-codeword PDSCH may have a different TBS + * (and therefore a different base graph) for each codeword. */ +static uint8_t dlsch_base_graph(uint32_t A, uint16_t R) +{ + const float code_rate = (float)R / 10240.0f; + return (A <= 292 || (A <= NR_MAX_PDSCH_TBS && code_rate <= 0.6667f) || code_rate <= 0.25f) ? 2 : 1; +} + void free_gNB_dlsch(NR_gNB_DLSCH_t *dlsch, uint16_t N_RB, const NR_DL_FRAME_PARMS *frame_parms) { int max_layers = (frame_parms->nb_antennas_tx < NR_MAX_NB_LAYERS) ? frame_parms->nb_antennas_tx : NR_MAX_NB_LAYERS; - uint16_t a_segments = MAX_NUM_NR_DLSCH_SEGMENTS_PER_LAYER * max_layers; + const int max_cw_layers = min(max_layers, 4); + uint16_t a_segments = MAX_NUM_NR_DLSCH_SEGMENTS_PER_LAYER * max_cw_layers; if (N_RB != 273) { a_segments = a_segments * N_RB; a_segments = a_segments / 273 + 1; } - if (dlsch->b) { - free16(dlsch->b, a_segments * 1056); - dlsch->b = NULL; + const size_t f_bytes = N_RB * frame_parms->symbols_per_slot * NR_NB_SC_PER_RB * 8 * max_cw_layers; + (void)f_bytes; /* free16 ignores its size argument on the supported allocators. */ + for (int q = 0; q < NR_MAX_NB_CODEWORDS; q++) { + if (dlsch->b[q]) { + free16(dlsch->b[q], a_segments * 1056); + dlsch->b[q] = NULL; + } + if (dlsch->f[q]) { + free16(dlsch->f[q], f_bytes); + dlsch->f[q] = NULL; + } + if (!dlsch->c[q]) + continue; + for (int r = 0; r < a_segments; r++) { + free(dlsch->c[q][r]); + dlsch->c[q][r] = NULL; + } + free(dlsch->c[q]); + dlsch->c[q] = NULL; } - if (dlsch->f) { - free16(dlsch->f, N_RB * frame_parms->symbols_per_slot * NR_NB_SC_PER_RB * 8 * NR_MAX_NB_LAYERS); - dlsch->f = NULL; - } - for (int r = 0; r < a_segments; r++) { - free(dlsch->c[r]); - dlsch->c[r] = NULL; - } - free(dlsch->c); } NR_gNB_DLSCH_t new_gNB_dlsch(NR_DL_FRAME_PARMS *frame_parms, uint16_t N_RB) { int max_layers = (frame_parms->nb_antennas_tx < NR_MAX_NB_LAYERS) ? frame_parms->nb_antennas_tx : NR_MAX_NB_LAYERS; - uint16_t a_segments = MAX_NUM_NR_DLSCH_SEGMENTS_PER_LAYER * max_layers; // number of segments to be allocated + const int max_cw_layers = min(max_layers, 4); + uint16_t a_segments = MAX_NUM_NR_DLSCH_SEGMENTS_PER_LAYER * max_cw_layers; // segments allocated per codeword if (N_RB != 273) { a_segments = a_segments * N_RB; @@ -59,28 +77,29 @@ NR_gNB_DLSCH_t new_gNB_dlsch(NR_DL_FRAME_PARMS *frame_parms, uint16_t N_RB) } LOG_D(PHY, "Allocating %d segments (MAX %d, N_PRB %d)\n", a_segments, MAX_NUM_NR_DLSCH_SEGMENTS_PER_LAYER, N_RB); - uint32_t dlsch_bytes = a_segments * 1056; // allocated bytes per segment + uint32_t dlsch_bytes = a_segments * 1056; NR_gNB_DLSCH_t dlsch = {0}; + const size_t f_bytes = N_RB * frame_parms->symbols_per_slot * NR_NB_SC_PER_RB * 8 * max_cw_layers; - dlsch.b = malloc16(dlsch_bytes); - AssertFatal(dlsch.b, "cannot allocate memory for dlsch.b\n"); - bzero(dlsch.b, dlsch_bytes); + for (int q = 0; q < NR_MAX_NB_CODEWORDS; q++) { + dlsch.b[q] = malloc16(dlsch_bytes); + AssertFatal(dlsch.b[q], "cannot allocate memory for dlsch.b[%d]\n", q); + bzero(dlsch.b[q], dlsch_bytes); - dlsch.c = (uint8_t **)malloc16(a_segments * sizeof(uint8_t *)); - for (int r = 0; r < a_segments; r++) { - // account for filler in first segment and CRCs for multiple segment case - // [hna] 8448 is the maximum CB size in NR - // 68*348 = 68*(maximum size of Zc) - // In section 5.3.2 in 38.212, the for loop is up to N + 2*Zc (maximum size of N is 66*Zc, therefore 68*Zc) - dlsch.c[r] = malloc16(8448); - AssertFatal(dlsch.c[r], "cannot allocate dlsch.c[%d]\n", r); - bzero(dlsch.c[r], 8448); + dlsch.c[q] = (uint8_t **)malloc16(a_segments * sizeof(uint8_t *)); + AssertFatal(dlsch.c[q], "cannot allocate memory for dlsch.c[%d]\n", q); + for (int r = 0; r < a_segments; r++) { + // 8448 is the maximum NR code-block size, including filler and CRC. + dlsch.c[q][r] = malloc16(8448); + AssertFatal(dlsch.c[q][r], "cannot allocate dlsch.c[%d][%d]\n", q, r); + bzero(dlsch.c[q][r], 8448); + } + + dlsch.f[q] = malloc16(f_bytes); + AssertFatal(dlsch.f[q], "cannot allocate dlsch.f[%d]\n", q); + bzero(dlsch.f[q], f_bytes); } - dlsch.f = malloc16(N_RB * frame_parms->symbols_per_slot * NR_NB_SC_PER_RB * 8 * NR_MAX_NB_LAYERS); - AssertFatal(dlsch.f, "cannot allocate dlsch->f\n"); - bzero(dlsch.f, N_RB * frame_parms->symbols_per_slot * NR_NB_SC_PER_RB * 8 * NR_MAX_NB_LAYERS); - return (dlsch); } @@ -98,92 +117,104 @@ int nr_dlsch_encoding(PHY_VARS_gNB *gNB, time_stats_t *dlsch_interleaving_stats, time_stats_t *dlsch_segmentation_stats) { - nrLDPC_TB_encoding_parameters_t TBs[n_dlsch]; + int n_tbs = 0; + for (int i = 0; i < n_dlsch; i++) { + const nfapi_nr_dl_tti_pdsch_pdu_rel15_t *rel15 = &dlsch_array[i].pdsch_pdu->pdsch_pdu_rel15; + AssertFatal(rel15->NrOfCodewords == nr_pdsch_num_codewords(rel15->nrOfLayers), + "PDSCH has %u codewords for %u layers\n", + rel15->NrOfCodewords, + rel15->nrOfLayers); + n_tbs += rel15->NrOfCodewords; + } + + nrLDPC_TB_encoding_parameters_t TBs[n_tbs]; memset(TBs, 0, sizeof(TBs)); + NR_gNB_DLSCH_t *tb_dlsch[n_tbs]; + uint8_t tb_cw[n_tbs]; int num_segments = 0; + int tb_index = 0; for (int i = 0; i < n_dlsch; i++) { NR_gNB_DLSCH_t *dlsch = &dlsch_array[i]; - - unsigned int crc = 1; const nfapi_nr_dl_tti_pdsch_pdu_rel15_t *rel15 = &dlsch->pdsch_pdu->pdsch_pdu_rel15; - uint32_t A = rel15->TBSize[0] << 3; - unsigned char *a = dlsch->pdu; - if (rel15->rnti != SI_RNTI) { - ws_trace_t tmp = {.nr = true, - .direction = DIRECTION_DOWNLINK, - .pdu_buffer = a, - .pdu_buffer_size = rel15->TBSize[0], - .ueid = 0, - .rntiType = WS_C_RNTI, - .rnti = rel15->rnti, - .sysFrame = frame, - .subframe = slot, - .harq_pid = 0, // difficult to find the harq pid here - .oob_event = 0, - .oob_event_value = 0}; - trace_pdu(&tmp); - } - NR_gNB_PHY_STATS_t *phy_stats = NULL; if (rel15->rnti != 0xFFFF) phy_stats = get_phy_stats(gNB, rel15->rnti); - if (phy_stats) { phy_stats->frame = frame; - phy_stats->dlsch_stats.total_bytes_tx += rel15->TBSize[0]; phy_stats->dlsch_stats.current_RI = rel15->nrOfLayers; phy_stats->dlsch_stats.current_Qm = rel15->qamModOrder[0]; } - int max_bytes = MAX_NUM_NR_DLSCH_SEGMENTS_PER_LAYER * rel15->nrOfLayers * 1056; - int B; - if (A > NR_MAX_PDSCH_TBS) { - // Add 24-bit crc (polynomial A) to payload - crc = crc24a(a, A) >> 8; - a[A >> 3] = ((uint8_t *)&crc)[2]; - a[1 + (A >> 3)] = ((uint8_t *)&crc)[1]; - a[2 + (A >> 3)] = ((uint8_t *)&crc)[0]; - // printf("CRC %x (A %d)\n",crc,A); - // printf("a0 %d a1 %d a2 %d\n", a[A>>3], a[1+(A>>3)], a[2+(A>>3)]); - B = A + 24; - AssertFatal((A / 8) + 4 <= max_bytes, "A %d is too big (A/8+4 = %d > %d)\n", A, (A / 8) + 4, max_bytes); - memcpy(dlsch->b, a, (A / 8) + 4); // why is this +4 if the CRC is only 3 bytes? - } else { - // Add 16-bit crc (polynomial A) to payload - crc = crc16(a, A) >> 16; - a[A >> 3] = ((uint8_t *)&crc)[1]; - a[1 + (A >> 3)] = ((uint8_t *)&crc)[0]; - // printf("CRC %x (A %d)\n",crc,A); - // printf("a0 %d a1 %d \n", a[A>>3], a[1+(A>>3)]); - B = A + 16; - AssertFatal((A / 8) + 3 <= max_bytes, "A %d is too big (A/8+3 = %d > %d)\n", A, (A / 8) + 3, max_bytes); - memcpy(dlsch->b, a, (A / 8) + 3); // using 3 bytes to mimic the case of 24 bit crc + for (int q = 0; q < rel15->NrOfCodewords; q++, tb_index++) { + const int cw_layers = nr_pdsch_layers_per_codeword(rel15->nrOfLayers, q); + const uint32_t A = rel15->TBSize[q] << 3; + unsigned char *a = dlsch->pdu[q]; + AssertFatal(a != NULL, "Missing PDSCH transport block for codeword %d\n", q); + + if (rel15->rnti != SI_RNTI) { + ws_trace_t tmp = {.nr = true, + .direction = DIRECTION_DOWNLINK, + .pdu_buffer = a, + .pdu_buffer_size = rel15->TBSize[q], + .ueid = 0, + .rntiType = WS_C_RNTI, + .rnti = rel15->rnti, + .sysFrame = frame, + .subframe = slot, + .harq_pid = 0, + .oob_event = 0, + .oob_event_value = 0}; + trace_pdu(&tmp); + } + if (phy_stats) + phy_stats->dlsch_stats.total_bytes_tx += rel15->TBSize[q]; + + unsigned int crc = 1; + const int max_bytes = MAX_NUM_NR_DLSCH_SEGMENTS_PER_LAYER * cw_layers * 1056; + int B; + if (A > NR_MAX_PDSCH_TBS) { + crc = crc24a(a, A) >> 8; + a[A >> 3] = ((uint8_t *)&crc)[2]; + a[1 + (A >> 3)] = ((uint8_t *)&crc)[1]; + a[2 + (A >> 3)] = ((uint8_t *)&crc)[0]; + B = A + 24; + AssertFatal((A / 8) + 4 <= max_bytes, "A %u is too big (%u > %d)\n", A, (A / 8) + 4, max_bytes); + memcpy(dlsch->b[q], a, (A / 8) + 4); + } else { + crc = crc16(a, A) >> 16; + a[A >> 3] = ((uint8_t *)&crc)[1]; + a[1 + (A >> 3)] = ((uint8_t *)&crc)[0]; + B = A + 16; + AssertFatal((A / 8) + 3 <= max_bytes, "A %u is too big (%u > %d)\n", A, (A / 8) + 3, max_bytes); + memcpy(dlsch->b[q], a, (A / 8) + 3); + } + + nrLDPC_TB_encoding_parameters_t *TB_parameters = &TBs[tb_index]; + tb_dlsch[tb_index] = dlsch; + tb_cw[tb_index] = q; + // A HARQ PID is not globally unique, so use the flattened TB index. + TB_parameters->harq_unique_pid = tb_index; + TB_parameters->BG = dlsch_base_graph(A, rel15->targetCodeRate[q]); + TB_parameters->A = A; + start_meas(dlsch_segmentation_stats); + TB_parameters->Kb = nr_segmentation(dlsch->b[q], + dlsch->c[q], + B, + &TB_parameters->C, + &TB_parameters->K, + &TB_parameters->Z, + &TB_parameters->F, + TB_parameters->BG); + stop_meas(dlsch_segmentation_stats); + + if (TB_parameters->C > MAX_NUM_NR_DLSCH_SEGMENTS_PER_LAYER * cw_layers) { + LOG_E(PHY, "nr_segmentation.c: too many segments %d, B %d, codeword layers %d\n", TB_parameters->C, B, cw_layers); + return -1; + } + num_segments += TB_parameters->C; } - - nrLDPC_TB_encoding_parameters_t *TB_parameters = &TBs[i]; - - // The harq_pid is not unique among the active HARQ processes in the instance so we use i instead - TB_parameters->harq_unique_pid = i; - TB_parameters->BG = rel15->maintenance_parms_v3.ldpcBaseGraph; - TB_parameters->A = A; - start_meas(dlsch_segmentation_stats); - TB_parameters->Kb = nr_segmentation(dlsch->b, - dlsch->c, - B, - &TB_parameters->C, - &TB_parameters->K, - &TB_parameters->Z, - &TB_parameters->F, - TB_parameters->BG); - stop_meas(dlsch_segmentation_stats); - - if (TB_parameters->C > MAX_NUM_NR_DLSCH_SEGMENTS_PER_LAYER * rel15->nrOfLayers) { - LOG_E(PHY, "nr_segmentation.c: too many segments %d, B %d\n", TB_parameters->C, B); - return (-1); - } - num_segments += TB_parameters->C; } nrLDPC_segment_encoding_parameters_t segments[num_segments]; @@ -191,19 +222,26 @@ int nr_dlsch_encoding(PHY_VARS_gNB *gNB, size_t segments_offset = 0; size_t dlsch_offset = 0; - for (int i = 0; i < n_dlsch; i++) { - NR_gNB_DLSCH_t *dlsch = &dlsch_array[i]; + for (int i = 0; i < n_tbs; i++) { + NR_gNB_DLSCH_t *dlsch = tb_dlsch[i]; const nfapi_nr_dl_tti_pdsch_pdu_rel15_t *rel15 = &dlsch->pdsch_pdu->pdsch_pdu_rel15; - + const int q = tb_cw[i]; + const int cw_layers = nr_pdsch_layers_per_codeword(rel15->nrOfLayers, q); nrLDPC_TB_encoding_parameters_t *TB_parameters = &TBs[i]; #ifdef DEBUG_DLSCH_CODING for (int r = 0; r < TB_parameters->C; r++) { LOG_D(PHY, "Encoder: B %d F %d \n", dlsch->B, TB_parameters->F); LOG_D(PHY, "start ldpc encoder segment %d/%d\n", r, TB_parameters->C); - LOG_D(PHY, "input %d %d %d %d %d \n", dlsch->c[r][0], dlsch->c[r][1], dlsch->c[r][2], dlsch->c[r][3], dlsch->c[r][4]); + LOG_D(PHY, + "input %d %d %d %d %d \n", + dlsch->c[q][r][0], + dlsch->c[q][r][1], + dlsch->c[q][r][2], + dlsch->c[q][r][3], + dlsch->c[q][r][4]); for (int cnt = 0; cnt < 22 * (TB_parameters->Z) / 8; cnt++) { - LOG_D(PHY, "%d ", dlsch->c[r][cnt]); + LOG_D(PHY, "%d ", dlsch->c[q][r][cnt]); } LOG_D(PHY, "\n"); } @@ -211,10 +249,10 @@ int nr_dlsch_encoding(PHY_VARS_gNB *gNB, int rbsize = dlsch->freq_alloc.num_rbs; TB_parameters->nb_rb = rbsize; - TB_parameters->Qm = rel15->qamModOrder[0]; - TB_parameters->mcs = rel15->mcsIndex[0]; - TB_parameters->nb_layers = rel15->nrOfLayers; - TB_parameters->rv_index = rel15->rvIndex[0]; + TB_parameters->Qm = rel15->qamModOrder[q]; + TB_parameters->mcs = rel15->mcsIndex[q]; + TB_parameters->nb_layers = cw_layers; + TB_parameters->rv_index = rel15->rvIndex[q]; int nb_re_dmrs = (rel15->dmrsConfigType == NFAPI_NR_DMRS_TYPE1) ? (6 * rel15->numDmrsCdmGrpsNoData) : (4 * rel15->numDmrsCdmGrpsNoData); @@ -223,8 +261,8 @@ int nr_dlsch_encoding(PHY_VARS_gNB *gNB, nb_re_dmrs, get_num_dmrs(rel15->dlDmrsSymbPos), dlsch->unav_res, - rel15->qamModOrder[0], - rel15->nrOfLayers); + rel15->qamModOrder[q], + cw_layers); TB_parameters->tbslbrm = rel15->maintenance_parms_v3.tbSizeLbrmBytes; TB_parameters->output = &output[dlsch_offset >> 3]; @@ -232,8 +270,8 @@ int nr_dlsch_encoding(PHY_VARS_gNB *gNB, for (int r = 0; r < TB_parameters->C; r++) { nrLDPC_segment_encoding_parameters_t *segment_parameters = &TB_parameters->segments[r]; - segment_parameters->c = dlsch->c[r]; - segment_parameters->E = nr_get_E(TB_parameters->G, TB_parameters->C, TB_parameters->Qm, rel15->nrOfLayers, r); + segment_parameters->c = dlsch->c[q][r]; + segment_parameters->E = nr_get_E(TB_parameters->G, TB_parameters->C, TB_parameters->Qm, cw_layers, r); reset_meas(&segment_parameters->ts_interleave); reset_meas(&segment_parameters->ts_rate_match); @@ -245,13 +283,14 @@ int nr_dlsch_encoding(PHY_VARS_gNB *gNB, /* output and its parts for each dlsch should be aligned on 64 bytes (or 8 * 64 bits) * => dlsch_offset should remain a multiple of 8 * 64 with enough offset to fit each dlsch */ - const size_t dlsch_size = rbsize * gNB->frame_parms.symbols_per_slot * NR_NB_SC_PER_RB * rel15->qamModOrder[0] * rel15->nrOfLayers; + const size_t dlsch_size = + rbsize * gNB->frame_parms.symbols_per_slot * NR_NB_SC_PER_RB * rel15->qamModOrder[q] * cw_layers; dlsch_offset += ceil_mod(dlsch_size, 8 * 64); } nrLDPC_slot_encoding_parameters_t slot_parameters = {.frame = frame, .slot = slot, - .nb_TBs = n_dlsch, + .nb_TBs = n_tbs, .threadPool = &gNB->threadPool, .tinput = tinput, .tprep = tprep, @@ -260,7 +299,7 @@ int nr_dlsch_encoding(PHY_VARS_gNB *gNB, .TBs = TBs}; gNB->nrLDPC_coding_interface.nrLDPC_coding_encoder(&slot_parameters); - for (int i = 0; i < n_dlsch; i++) { + for (int i = 0; i < n_tbs; i++) { nrLDPC_TB_encoding_parameters_t *TB_parameters = &TBs[i]; for (int r = 0; r < TB_parameters->C; r++) { nrLDPC_segment_encoding_parameters_t *segment_parameters = &TB_parameters->segments[r]; diff --git a/openair1/PHY/NR_TRANSPORT/nr_sch_dmrs.h b/openair1/PHY/NR_TRANSPORT/nr_sch_dmrs.h index 98b42b87c2..d537deff3c 100644 --- a/openair1/PHY/NR_TRANSPORT/nr_sch_dmrs.h +++ b/openair1/PHY/NR_TRANSPORT/nr_sch_dmrs.h @@ -19,4 +19,12 @@ uint16_t get_dmrs_freq_idx(uint16_t n, uint8_t k_prime, uint8_t delta, uint8_t d uint8_t get_l0(uint16_t dlDmrsSymbPos); +/* For a double-symbol PDSCH DMRS occasion, l'=1 on the second symbol of + * each adjacent pair and l'=0 on the first. Single-symbol occasions always + * use l'=0. */ +static inline uint8_t get_pdsch_dmrs_l_prime(uint16_t dmrs_symbol_mask, uint8_t symbol) +{ + return symbol > 0 && ((dmrs_symbol_mask >> (symbol - 1)) & 0x1); +} + #endif diff --git a/openair1/PHY/NR_UE_ESTIMATION/nr_dl_channel_estimation.c b/openair1/PHY/NR_UE_ESTIMATION/nr_dl_channel_estimation.c index 2ed339de8a..dc108e1d89 100644 --- a/openair1/PHY/NR_UE_ESTIMATION/nr_dl_channel_estimation.c +++ b/openair1/PHY/NR_UE_ESTIMATION/nr_dl_channel_estimation.c @@ -1282,7 +1282,8 @@ void nr_pdsch_channel_estimation(PHY_VARS_NR_UE *ue, float beta_dmrs_pdsch = get_beta_dmrs(dlsch->n_dmrs_cdm_groups, config_type == NFAPI_NR_DMRS_TYPE2); int16_t dmrs_scaling = (int16_t)((1 / beta_dmrs_pdsch) * (1 << 14)); const uint32_t *gold = nr_gold_pdsch(fp->N_RB_DL, fp->symbols_per_slot, dlsch->dlDmrsScramblingId, dlsch->nscid, slot, symbol); - nr_pdsch_dmrs_rx(fp->Ncp, gold, pilot, 1000 + p, 0, nb_rb_pdsch + rb_offset, config_type, dmrs_scaling); + const uint8_t l_prime = get_pdsch_dmrs_l_prime(dlsch->dlDmrsSymbPos, symbol); + nr_pdsch_dmrs_rx(fp->Ncp, gold, pilot, 1000 + p, l_prime, nb_rb_pdsch + rb_offset, config_type, dmrs_scaling); delay_t delay = {0}; diff --git a/openair1/PHY/NR_UE_TRANSPORT/csi_rx.c b/openair1/PHY/NR_UE_TRANSPORT/csi_rx.c index 8ade018829..c0b87ec563 100644 --- a/openair1/PHY/NR_UE_TRANSPORT/csi_rx.c +++ b/openair1/PHY/NR_UE_TRANSPORT/csi_rx.c @@ -15,6 +15,7 @@ #include #include #include +#include #include "executables/nr-softmodem-common.h" #include "nr_transport_proto_ue.h" @@ -22,6 +23,101 @@ #include "common/utils/nr/nr_common.h" #include "PHY/NR_UE_ESTIMATION/filt16a_32.h" +// nakaolab CSI muting study: float Gram matrix + Jacobi eigenvalues for RI and +// spatial-spectrum telemetry. MAX_ANT is eight on this platform; the UE still +// reports at most NR_MAX_NB_LAYERS (four) because that is the supported PDSCH +// decoder/scheduler ceiling. +// Sorts eigenvalues in descending order on output. +static void nr_hermitian_eigenvalues(const int N, + float A_re[MAX_ANT][MAX_ANT], + float A_im[MAX_ANT][MAX_ANT], + float eigenvalues[MAX_ANT]) +{ + for (int sweep = 0; sweep < 64; sweep++) { + float off_norm = 0; + float diag_norm = 0; + for (int i = 0; i < N; i++) + diag_norm += A_re[i][i] * A_re[i][i]; + for (int i = 0; i < N; i++) + for (int j = i + 1; j < N; j++) + off_norm += A_re[i][j] * A_re[i][j] + A_im[i][j] * A_im[i][j]; + if (off_norm <= 1e-10f * (diag_norm + 1.0f)) + break; + + for (int p = 0; p < N - 1; p++) { + for (int q = p + 1; q < N; q++) { + float re_pq = A_re[p][q], im_pq = A_im[p][q]; + float mag = sqrtf(re_pq * re_pq + im_pq * im_pq); + if (mag < 1e-12f) + continue; + + float cos_phi = re_pq / mag; + float sin_phi = im_pq / mag; + + // Phase elimination: rotate column/row q so A[p][q] is real. + // Patch 1: skip k==q (was a bug — corrupted diagonal A[q][q]). + for (int k = 0; k < N; k++) { + if (k == q) continue; + float old_re = A_re[k][q], old_im = A_im[k][q]; + A_re[k][q] = old_re * cos_phi + old_im * sin_phi; + A_im[k][q] = -old_re * sin_phi + old_im * cos_phi; + } + for (int k = 0; k < N; k++) { + if (k == q) continue; + A_re[q][k] = A_re[k][q]; + A_im[q][k] = -A_im[k][q]; + } + + // Real Jacobi rotation + float d = A_re[q][q] - A_re[p][p]; + float t; + if (fabsf(d) < 1e-12f) { + t = 1.0f; + } else { + float tau = d / (2.0f * mag); + t = ((tau >= 0) ? 1.0f : -1.0f) / (fabsf(tau) + sqrtf(1.0f + tau * tau)); + } + float c = 1.0f / sqrtf(1.0f + t * t); + float s = t * c; + + A_re[p][p] -= t * mag; + A_re[q][q] += t * mag; + A_re[p][q] = 0; + A_im[p][q] = 0; + A_re[q][p] = 0; + A_im[q][p] = 0; + + for (int r = 0; r < N; r++) { + if (r == p || r == q) continue; + float rp_re = A_re[r][p], rp_im = A_im[r][p]; + float rq_re = A_re[r][q], rq_im = A_im[r][q]; + A_re[r][p] = c * rp_re - s * rq_re; + A_im[r][p] = c * rp_im - s * rq_im; + A_re[p][r] = A_re[r][p]; + A_im[p][r] = -A_im[r][p]; + A_re[r][q] = s * rp_re + c * rq_re; + A_im[r][q] = s * rp_im + c * rq_im; + A_re[q][r] = A_re[r][q]; + A_im[q][r] = -A_im[r][q]; + } + } + } + } + + // Patch 2: clamp eigenvalues to >=0 (numerical errors can produce negative values) + for (int i = 0; i < N; i++) + eigenvalues[i] = A_re[i][i] > 0.0f ? A_re[i][i] : 0.0f; + + // Sort descending + for (int i = 0; i < N - 1; i++) + for (int j = i + 1; j < N; j++) + if (eigenvalues[j] > eigenvalues[i]) { + float tmp = eigenvalues[i]; + eigenvalues[i] = eigenvalues[j]; + eigenvalues[j] = tmp; + } +} + //#define NR_CSIRS_DEBUG //#define NR_CSIIM_DEBUG @@ -65,6 +161,16 @@ void nr_det_A_MF_2x2(int32_t *a_mf_00, } } +typedef struct { + int samples; + int dimensions; + float eig_rel_db[NR_MAX_NB_LAYERS]; + // Retained diagonal Gram-matrix energy for active prefix sizes 1/2/4/8, + // relative to all configured CSI-RS ports. This is an offline feature for + // testing whether CSI adds value to antenna-muting decisions. + float prefix_rel_db[4]; +} nr_csi_muting_summary_t; + void nr_squared_matrix_element(int32_t *a, int32_t *a_sq, const unsigned short nb_rb) { @@ -421,21 +527,114 @@ static int nr_csi_rs_ri_estimation(const PHY_VARS_NR_UE *ue, const uint8_t N_ports, c16_t csi_rs_estimated_channel_freq[][N_ports][ue->frame_parms.ofdm_symbol_size], const int16_t log2_maxh, - uint8_t *rank_indicator) + uint8_t *rank_indicator, + nr_csi_muting_summary_t *muting_summary) { const NR_DL_FRAME_PARMS *frame_parms = &ue->frame_parms; const int16_t cond_dB_threshold = 5; int count = 0; *rank_indicator = 0; + if (muting_summary != NULL) + memset(muting_summary, 0, sizeof(*muting_summary)); if (ue->frame_parms.nb_antennas_rx == 1 || N_ports == 1) { return 0; - } else if( !(ue->frame_parms.nb_antennas_rx == 2 && N_ports == 2) ) { - LOG_W(NR_PHY, "Rank indicator computation is not implemented for %i x %i system\n", - ue->frame_parms.nb_antennas_rx, N_ports); + } + + if (N_ports > MAX_ANT || frame_parms->nb_antennas_rx > MAX_ANT) { + LOG_W(NR_PHY, + "CSI spatial-spectrum computation exceeds platform bound for %i x %i system\n", + frame_parms->nb_antennas_rx, + N_ports); return -1; } + const int physical_rank = N_ports < frame_parms->nb_antennas_rx ? N_ports : frame_parms->nb_antennas_rx; + const int max_rank = physical_rank < NR_MAX_NB_LAYERS ? physical_rank : NR_MAX_NB_LAYERS; + if (max_rank < 2) { + return 0; + } + + // 2x2 keeps the existing condition-number path below. + // For >2 ports, use Patches 1-3: float Gram matrix + Jacobi eigenvalues + rank voting. + if (!(ue->frame_parms.nb_antennas_rx == 2 && N_ports == 2)) { + int rank_count[NR_MAX_NB_LAYERS] = {0}; + double eig_sum[MAX_ANT] = {0}; + double port_power[MAX_ANT] = {0}; + const float eig_ratio_dB_threshold = 8.0f; + const int sc_stride = physical_rank > 4 ? 4 : 1; + for (int rb = csirs_config_pdu->start_rb; rb < (csirs_config_pdu->start_rb + csirs_config_pdu->nr_of_rbs); rb++) { + if (csirs_config_pdu->freq_density <= 1 && csirs_config_pdu->freq_density != (rb % 2)) continue; + uint16_t k = rb * NR_NB_SC_PER_RB; + for (int sc_idx = 0; sc_idx < NR_NB_SC_PER_RB; sc_idx += sc_stride) { + // Patch 3: float Gram matrix A = H^H H computed directly from channel estimates + // (avoids int16 saturation that capped RI at 2 in 4-RX setups). + float A_re[MAX_ANT][MAX_ANT] = {{0}}, A_im[MAX_ANT][MAX_ANT] = {{0}}; + for (int rx = 0; rx < frame_parms->nb_antennas_rx; rx++) { + for (int p1 = 0; p1 < N_ports; p1++) { + c16_t h1 = csi_rs_estimated_channel_freq[rx][p1][k + sc_idx]; + float h1_re = (float)h1.r, h1_im = (float)h1.i; + for (int p2 = 0; p2 < N_ports; p2++) { + c16_t h2 = csi_rs_estimated_channel_freq[rx][p2][k + sc_idx]; + float h2_re = (float)h2.r, h2_im = (float)h2.i; + A_re[p1][p2] += h1_re * h2_re + h1_im * h2_im; + A_im[p1][p2] += -h1_im * h2_re + h1_re * h2_im; + } + } + } + for (int p = 0; p < N_ports; p++) + port_power[p] += A_re[p][p]; + float eig[MAX_ANT] = {0}; + nr_hermitian_eigenvalues(N_ports, A_re, A_im, eig); + if (eig[0] < 1e-6f) continue; + rank_count[0]++; + for (int r = 0; r < N_ports; r++) + eig_sum[r] += eig[r]; + for (int r = 1; r < max_rank; r++) { + if (eig[r] > 1e-6f && 10.0f * log10f(eig[0] / eig[r]) < eig_ratio_dB_threshold) { + rank_count[r]++; + } else { + break; + } + } + } + } + int total_sc = rank_count[0]; + if (total_sc > 0) { + for (int r = max_rank - 1; r >= 1; r--) { + if (rank_count[r] > total_sc / 2) { + *rank_indicator = r; + break; + } + } + if (muting_summary != NULL) { + muting_summary->samples = total_sc; + muting_summary->dimensions = physical_rank; + for (int r = 0; r < NR_MAX_NB_LAYERS; r++) { + muting_summary->eig_rel_db[r] = + r < physical_rank && eig_sum[r] > 0.0 && eig_sum[0] > 0.0 + ? 10.0f * log10f((float)(eig_sum[r] / eig_sum[0])) + : -INFINITY; + } + double full_power = 0.0; + for (int p = 0; p < N_ports; p++) + full_power += port_power[p]; + const int prefix_size[4] = {1, 2, 4, 8}; + for (int candidate = 0; candidate < 4; candidate++) { + const int keep = prefix_size[candidate] < N_ports ? prefix_size[candidate] : N_ports; + double prefix_power = 0.0; + for (int p = 0; p < keep; p++) + prefix_power += port_power[p]; + muting_summary->prefix_rel_db[candidate] = + prefix_power > 0.0 && full_power > 0.0 + ? 10.0f * log10f((float)(prefix_power / full_power)) + : -INFINITY; + } + } + } + return 0; + } + /* Example 2x2: Hh x H = * | conjch00 conjch10 | x | ch00 ch01 | = | conjch00*ch00+conjch10*ch10 conjch00*ch01+conjch10*ch11 | * | conjch01 conjch11 | | ch10 ch11 | | conjch01*ch00+conjch11*ch10 conjch01*ch01+conjch11*ch11 | @@ -558,6 +757,9 @@ static int nr_csi_rs_pmi_estimation(const PHY_VARS_NR_UE *ue, int32_t *precoded_sinr_dB) { const NR_DL_FRAME_PARMS *frame_parms = &ue->frame_parms; + // RF simulators can report exactly zero interference/noise. Keep every PMI + // rank path finite, including the local rank-3/4 extension below. + const uint32_t non_zero_noise_power = interference_plus_noise_power == 0 ? 1 : interference_plus_noise_power; // i1 is a three-element vector in the form of [i11 i12 i13], when CodebookType is specified as 'Type1SinglePanel'. // Note that i13 is not applicable when the number of transmission layers is one of {1, 5, 6, 7, 8}. @@ -587,7 +789,7 @@ static int nr_csi_rs_pmi_estimation(const PHY_VARS_NR_UE *ue, if (count > 0) { const int64_t avg_signal_power = signal_power / count; // Non RF devices like ZMQ has virtually zero noise. So here we make noise as 1 to return maximum sinr. - const uint32_t sinr = avg_signal_power / ((interference_plus_noise_power == 0) ? 1 : interference_plus_noise_power); + const uint32_t sinr = avg_signal_power / non_zero_noise_power; *precoded_sinr_dB = dB_fixed(sinr); } @@ -640,7 +842,7 @@ static int nr_csi_rs_pmi_estimation(const PHY_VARS_NR_UE *ue, for(int p = 0; p<4; p++) { int64_t power_re = sum2[p].r - (sum[p].r >> log2_re) * (sum[p].r >> log2_re); int64_t power_im = sum2[p].i - (sum[p].i >> log2_re) * (sum[p].i >> log2_re); - tested_precoded_sinr[p] = (power_re + power_im) / interference_plus_noise_power; + tested_precoded_sinr[p] = (power_re + power_im) / non_zero_noise_power; } if(rank_indicator == 0) { @@ -655,6 +857,29 @@ static int nr_csi_rs_pmi_estimation(const PHY_VARS_NR_UE *ue, *precoded_sinr_dB = dB_fixed((tested_precoded_sinr[i2[0]] + tested_precoded_sinr[i2[0]+2])>>1); } + } else if (rank_indicator == 2 || rank_indicator == 3) { + // RI=3 or RI=4 (4-port): Use identity-like precoder (i2=0). + // gNB uses identity_precoding_matrix=1 anyway, so feedback isn't acted on + // beyond the SINR-derived CQI. Without this branch, CQI is reported as 0 + // and the gNB conservatively backs off rank/MCS. + // Restored from commit 3117df91cb (lost in upstream csi_rx.c refactor). + int nlayers = rank_indicator + 1; + int64_t total_power = 0; + for (int rb = csirs_config_pdu->start_rb; + rb < (csirs_config_pdu->start_rb + csirs_config_pdu->nr_of_rbs); rb++) { + if (csirs_config_pdu->freq_density <= 1 && csirs_config_pdu->freq_density != (rb % 2)) + continue; + uint16_t k = rb * NR_NB_SC_PER_RB; + for (int ant_rx = 0; ant_rx < frame_parms->nb_antennas_rx; ant_rx++) { + for (int p = 0; p < nlayers; p++) { + const c16_t h = csi_rs_estimated_channel_freq[ant_rx][p][k]; + total_power += (int64_t)h.r * h.r + (int64_t)h.i * h.i; + } + } + } + i2[0] = 0; + if (total_power > 0) + *precoded_sinr_dB = dB_fixed((total_power / nlayers) / non_zero_noise_power); } else { LOG_W(NR_PHY, "PMI computation is not implemented for rank indicator %i\n", rank_indicator+1); return -1; @@ -895,6 +1120,7 @@ void nr_ue_csi_rs_procedures(PHY_VARS_NR_UE *ue, &noise_power); uint8_t rank_indicator = 0; + nr_csi_muting_summary_t muting_summary = {0}; // bit 1 in bitmap to indicate RI measurment if (csirs_config_pdu->measurement_bitmap & 2) { nr_csi_rs_ri_estimation(ue, @@ -902,7 +1128,8 @@ void nr_ue_csi_rs_procedures(PHY_VARS_NR_UE *ue, mapping_parms.ports, csi_rs_estimated_channel_freq, log2_maxh, - &rank_indicator); + &rank_indicator, + &muting_summary); } uint8_t i1[3] = {0}; @@ -926,6 +1153,31 @@ void nr_ue_csi_rs_procedures(PHY_VARS_NR_UE *ue, nr_csi_rs_cqi_estimation(precoded_sinr_dB, &cqi); } + const char *muting_telemetry = getenv("OAI_CSI_MUTING_TELEMETRY"); + if (muting_summary.samples > 0 && muting_telemetry != NULL && strcmp(muting_telemetry, "1") == 0) { + LOG_I(NR_PHY, + "[CSI_MUTING] frame=%d slot=%d ue=%d rx=%u ports=%u samples=%d " + "ri=%u cqi=%u sinr_db=%d eig_rel_db=%.2f,%.2f,%.2f,%.2f " + "prefix_rel_db=%.2f,%.2f,%.2f,%.2f\n", + proc->frame_rx, + proc->nr_slot_rx, + ue->Mod_id, + frame_parms->nb_antennas_rx, + mapping_parms.ports, + muting_summary.samples, + rank_indicator + 1, + cqi, + precoded_sinr_dB, + muting_summary.eig_rel_db[0], + muting_summary.eig_rel_db[1], + muting_summary.eig_rel_db[2], + muting_summary.eig_rel_db[3], + muting_summary.prefix_rel_db[0], + muting_summary.prefix_rel_db[1], + muting_summary.prefix_rel_db[2], + muting_summary.prefix_rel_db[3]); + } + switch (csirs_config_pdu->measurement_bitmap) { case 1 : LOG_I(NR_PHY, "[UE %d] RSRP = %i dBm\n", ue->Mod_id, rsrp_dBm); diff --git a/openair1/PHY/NR_UE_TRANSPORT/dci_nr.c b/openair1/PHY/NR_UE_TRANSPORT/dci_nr.c index 2e4811860b..edad1b7c60 100644 --- a/openair1/PHY/NR_UE_TRANSPORT/dci_nr.c +++ b/openair1/PHY/NR_UE_TRANSPORT/dci_nr.c @@ -290,14 +290,19 @@ static void nr_pdcch_channel_compensation(int arraySz, static void nr_pdcch_detection_mrc(int nb_ant, int sz, c16_t rxdataF_comp[][sz]) { c16_t *rx0 = rxdataF_comp[0]; - // MRC on each re of rb - // input always aligned and accepting tail padding to process all actual samples - for (int a = 1; a < nb_ant; a++) { - c16_t *rx = rxdataF_comp[a]; - for (int i = 0; i < sz; i += 4) { - *(simde__m128i *)(rx0 + i) = simde_mm_adds_epi16(simde_mm_srai_epi16(*(simde__m128i *)(rx0 + i), 1), - simde_mm_srai_epi16(*(simde__m128i *)(rx + i), 1)); + /* Accumulate every matched-filter branch with equal weight. The former + * pairwise right shift repeatedly attenuated the existing accumulator, so + * with eight RX antennas branch 0 was divided by 128 and PDCCH decoding + * failed even at high SNR. Use a wide accumulator and saturate once. */ + for (int i = 0; i < sz; i++) { + int32_t real = 0; + int32_t imag = 0; + for (int a = 0; a < nb_ant; a++) { + real += rxdataF_comp[a][i].r; + imag += rxdataF_comp[a][i].i; } + rx0[i].r = min(max(real, INT16_MIN), INT16_MAX); + rx0[i].i = min(max(imag, INT16_MIN), INT16_MAX); } } diff --git a/openair1/PHY/NR_UE_TRANSPORT/nr_dlsch_demodulation.c b/openair1/PHY/NR_UE_TRANSPORT/nr_dlsch_demodulation.c index c94dfef849..43e6ed693a 100644 --- a/openair1/PHY/NR_UE_TRANSPORT/nr_dlsch_demodulation.c +++ b/openair1/PHY/NR_UE_TRANSPORT/nr_dlsch_demodulation.c @@ -852,6 +852,164 @@ static void nr_dlsch_mmse(uint32_t rx_size_symbol, } } +/* Joint linear MMSE detector for rank 5--8 PDSCH. + * + * The legacy fixed-point detector above constructs cofactors recursively and + * is deliberately limited to four layers. Two-codeword PDSCH still shares + * one physical MIMO channel, so detecting each four-layer codeword in + * isolation is incorrect. This path builds H^H H over all active layers for + * every data RE, inverts the Hermitian matrix with pivoted Gauss-Jordan + * elimination, and equalizes all layers together. The right half of the + * augmented matrix is retained to remove the MMSE bias before the existing + * QAM soft demappers are used. + */ +static int nr_dlsch_mmse_high_rank(uint32_t rx_size_symbol, + uint8_t n_rx, + uint8_t nl, + const c16_t rxdataF_ext[][rx_size_symbol], + int32_t dl_ch_estimates_ext[][rx_size_symbol], + c16_t rxdataF_comp[][nl][n_rx][rx_size_symbol], + uint8_t symbol, + int length, + uint32_t noise_var) +{ + AssertFatal(nl > 4 && nl <= NR_MAX_NB_LAYERS, "Invalid high-rank PDSCH layer count %u\n", nl); + if (n_rx < nl) { + LOG_E(NR_PHY, "Rank-%u PDSCH requires at least %u UE RX antennas, only %u configured\n", nl, nl, n_rx); + return -1; + } + + const double q15 = 32768.0; + const double normalized_noise = (double)noise_var / (q15 * q15); + + for (int re = 0; re < length; re++) { + double complex gram[NR_MAX_NB_LAYERS][NR_MAX_NB_LAYERS] = {{0}}; + double complex rhs[NR_MAX_NB_LAYERS] = {0}; + + for (int aarx = 0; aarx < n_rx; aarx++) { + const c16_t y_fixed = rxdataF_ext[aarx][re]; + const double complex y = (double)y_fixed.r + I * (double)y_fixed.i; + double complex h[NR_MAX_NB_LAYERS]; + for (int l = 0; l < nl; l++) { + const c16_t *ch = (const c16_t *)dl_ch_estimates_ext[l * n_rx + aarx]; + h[l] = (double)ch[re].r / q15 + I * ((double)ch[re].i / q15); + rhs[l] += conj(h[l]) * y; + } + for (int row = 0; row < nl; row++) + for (int col = 0; col < nl; col++) + gram[row][col] += conj(h[row]) * h[col]; + } + + double trace = 0.0; + for (int l = 0; l < nl; l++) + trace += creal(gram[l][l]); + const double average_gain = trace / nl; + /* Channel-estimation residuals include some uncancelled CDM energy before + * the two front-loaded DMRS symbols are averaged. Bound that estimate so + * it regularizes an ill-conditioned channel without suppressing a valid + * spatial stream. */ + const double ridge_floor = fmax(average_gain * 1e-7, 1e-12); + const double ridge = fmax(ridge_floor, fmin(normalized_noise, average_gain * 0.25)); + + double complex aug[NR_MAX_NB_LAYERS][2 * NR_MAX_NB_LAYERS] = {{0}}; + for (int row = 0; row < nl; row++) { + for (int col = 0; col < nl; col++) + aug[row][col] = gram[row][col] + (row == col ? ridge : 0.0); + aug[row][nl + row] = 1.0; + } + + for (int col = 0; col < nl; col++) { + int pivot_row = col; + double pivot_abs = cabs(aug[col][col]); + for (int row = col + 1; row < nl; row++) { + const double candidate = cabs(aug[row][col]); + if (candidate > pivot_abs) { + pivot_abs = candidate; + pivot_row = row; + } + } + if (pivot_abs < 1e-15) { + LOG_W(NR_PHY, "Singular rank-%u PDSCH channel matrix at RE %d\n", nl, re); + return -1; + } + if (pivot_row != col) { + for (int j = 0; j < 2 * nl; j++) { + const double complex tmp = aug[col][j]; + aug[col][j] = aug[pivot_row][j]; + aug[pivot_row][j] = tmp; + } + } + + const double complex pivot = aug[col][col]; + for (int j = 0; j < 2 * nl; j++) + aug[col][j] /= pivot; + for (int row = 0; row < nl; row++) { + if (row == col) + continue; + const double complex factor = aug[row][col]; + if (cabs(factor) == 0.0) + continue; + for (int j = 0; j < 2 * nl; j++) + aug[row][j] -= factor * aug[col][j]; + } + } + + for (int layer = 0; layer < nl; layer++) { + double complex detected = 0.0; + for (int col = 0; col < nl; col++) + detected += aug[layer][nl + col] * rhs[col]; + /* diag((G + lambda I)^-1 G) = 1 - lambda*diag(inv), + * so divide out the linear-MMSE attenuation before applying fixed QAM + * decision boundaries. */ + const double gain = fmax(0.05, 1.0 - ridge * creal(aug[layer][nl + layer])); + detected /= gain; + /* Channel-estimation normalization leaves the equalized constellation + * sqrt(2) above the native NR modulation-table scale. At 256-QAM that + * clips the outer levels in Q15 (15/sqrt(170) * 32768 * sqrt(2) is + * greater than INT16_MAX), making the two outermost levels + * indistinguishable. Restore the native scale before quantization. */ + detected *= 0.7071067811865475244; + const long real = lround(creal(detected)); + const long imag = lround(cimag(detected)); + rxdataF_comp[symbol][layer][0][re].r = min(max(real, -32767), 32767); + rxdataF_comp[symbol][layer][0][re].i = min(max(imag, -32767), 32767); + } + } + return 0; +} + +static void set_high_rank_qam_thresholds(uint8_t mod_order, + int length, + c16_t *mag, + c16_t *magb, + c16_t *magr) +{ + /* The joint detector is rescaled to the native NR modulation-table domain + * before Q15 quantization. Those tables include the per-I/Q 1/sqrt(2) + * factor, unlike the legacy matched-filter output for which the QAM*_n + * constants are used directly. Scale the decision boundaries likewise. */ +#define HIGH_RANK_QAM_BOUNDARY(value) (((int32_t)(value) * ONE_OVER_SQRT2_Q15 + (1 << 14)) >> 15) + int16_t a = 0; + int16_t b = 0; + int16_t c = 0; + if (mod_order == 4) + a = HIGH_RANK_QAM_BOUNDARY(QAM16_n1); + else if (mod_order == 6) { + a = HIGH_RANK_QAM_BOUNDARY(QAM64_n1); + b = HIGH_RANK_QAM_BOUNDARY(QAM64_n2); + } else if (mod_order == 8) { + a = HIGH_RANK_QAM_BOUNDARY(QAM256_n1); + b = HIGH_RANK_QAM_BOUNDARY(QAM256_n2); + c = HIGH_RANK_QAM_BOUNDARY(QAM256_n3); + } +#undef HIGH_RANK_QAM_BOUNDARY + for (int re = 0; re < length; re++) { + mag[re] = (c16_t){a, a}; + magb[re] = (c16_t){b, b}; + magr[re] = (c16_t){c, c}; + } +} + static void nr_dlsch_layer_demapping(const uint8_t Nl, const uint8_t mod_order, const int llrLayerSize, @@ -875,6 +1033,10 @@ static void nr_dlsch_layer_demapping(const uint8_t Nl, case 2: case 3: case 4: + case 5: + case 6: + case 7: + case 8: for (int i = s0; i < (s0 + s1); i++) { int m = 0; for (int j = 0; j < re_len[i]; j++) { @@ -1027,7 +1189,7 @@ int nr_rx_pdsch(PHY_VARS_NR_UE *ue, nl, fp, csi_res_bitmap, - ue->chest_time); + nl > 4 ? 1 : ue->chest_time); stop_meas_nr_ue_phy(ue, DLSCH_EXTRACT_RBS_STATS); if (scope_req->copy_chanest_to_scope) { size_t size = sizeof(c16_t) * nb_rb_pdsch * NR_NB_SC_PER_RB; @@ -1154,22 +1316,42 @@ int nr_rx_pdsch(PHY_VARS_NR_UE *ue, //---------------------------------------------------------- // Disable correlation measurement for optimizing UE start_meas_nr_ue_phy(ue, DLSCH_CHANNEL_COMPENSATION_STATS); - nr_dlsch_channel_compensation(rx_size_symbol, - nbRx, - nl, - rxdataF_ext, - dl_ch_estimates_ext, - dl_ch_mag[symbol], - dl_ch_magb[symbol], - dl_ch_magr[symbol], - rxdataF_comp, - NULL, - fp, - symbol, - nb_re_pdsch, - dlsch->cw_info.qamModOrder, - nb_rb_pdsch, - *log2_maxh); // log2_maxh+I0_shift + if (nl > 4) { + if (nb_re_pdsch + && nr_dlsch_mmse_high_rank(rx_size_symbol, + n_rx, + nl, + rxdataF_ext, + dl_ch_estimates_ext, + rxdataF_comp, + symbol, + nb_re_pdsch, + nvar) + < 0) + return -1; + set_high_rank_qam_thresholds(dlsch->cw_info.qamModOrder, + nb_re_pdsch, + dl_ch_mag[symbol][0][0], + dl_ch_magb[symbol][0][0], + dl_ch_magr[symbol][0][0]); + } else { + nr_dlsch_channel_compensation(rx_size_symbol, + nbRx, + nl, + rxdataF_ext, + dl_ch_estimates_ext, + dl_ch_mag[symbol], + dl_ch_magb[symbol], + dl_ch_magr[symbol], + rxdataF_comp, + NULL, + fp, + symbol, + nb_re_pdsch, + dlsch->cw_info.qamModOrder, + nb_rb_pdsch, + *log2_maxh); // log2_maxh+I0_shift + } stop_meas_nr_ue_phy(ue, DLSCH_CHANNEL_COMPENSATION_STATS); if (meas_enabled) { LOG_D(PHY, @@ -1203,7 +1385,7 @@ int nr_rx_pdsch(PHY_VARS_NR_UE *ue, } start_meas_nr_ue_phy(ue, DLSCH_MRC_MMSE_STATS); - if (n_rx > 1) { + if (nl <= 4 && n_rx > 1) { nr_dlsch_detection_mrc(rx_size_symbol, nl, n_rx, diff --git a/openair1/PHY/defs_gNB.h b/openair1/PHY/defs_gNB.h index 1ca19deda0..850e5f8635 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 @@ -105,13 +106,13 @@ typedef struct { /// freq allocation information freq_alloc_bitmap_t freq_alloc; /// pointer to pdu from MAC interface (this is "a" in 36.212) - uint8_t *pdu; + uint8_t *pdu[NR_MAX_NB_CODEWORDS]; /// Pointer to the payload - uint8_t *b; + uint8_t *b[NR_MAX_NB_CODEWORDS]; /// Pointers to transport block segments - uint8_t **c; + uint8_t **c[NR_MAX_NB_CODEWORDS]; /// Interleaver outputs - uint8_t *f; + uint8_t *f[NR_MAX_NB_CODEWORDS]; /// REs unavailable for DLSCH (overlapping with PTRS, CSIRS etc.) uint32_t unav_res; } NR_gNB_DLSCH_t; @@ -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/PHY/defs_nr_UE.h b/openair1/PHY/defs_nr_UE.h index 03615427c1..834a61fd23 100644 --- a/openair1/PHY/defs_nr_UE.h +++ b/openair1/PHY/defs_nr_UE.h @@ -302,11 +302,11 @@ typedef struct PHY_VARS_NR_UE_s { /// \brief Total gain of the RX chain (antenna to baseband I/Q) This is a function of rx_gain_mode (and the corresponding gain) and the rx_gain of the card. uint32_t rx_total_gain_dB; /// \brief Total gains with maximum RF gain stage (ExpressMIMO2/Lime) - uint32_t rx_gain_max[4]; + uint32_t rx_gain_max[MAX_ANT]; /// \brief Total gains with medium RF gain stage (ExpressMIMO2/Lime) - uint32_t rx_gain_med[4]; + uint32_t rx_gain_med[MAX_ANT]; /// \brief Total gains with bypassed RF gain stage (ExpressMIMO2/Lime) - uint32_t rx_gain_byp[4]; + uint32_t rx_gain_byp[MAX_ANT]; /// \brief Current transmit power int16_t tx_power_dBm[NR_MAX_SLOTS_PER_FRAME]; /// \brief Total number of REs in current transmission diff --git a/openair1/SCHED_NR/nr_ru_procedures.c b/openair1/SCHED_NR/nr_ru_procedures.c index 68dc731f64..cbd05f1cd9 100644 --- a/openair1/SCHED_NR/nr_ru_procedures.c +++ b/openair1/SCHED_NR/nr_ru_procedures.c @@ -19,8 +19,36 @@ #include "assertions.h" +#include +#include +#include +#include #include +static pthread_once_t tx_chain_telemetry_once = PTHREAD_ONCE_INIT; +static bool tx_chain_telemetry = false; +static atomic_uint tx_chain_proven_mask = ATOMIC_VAR_INIT(0); + +static void configure_tx_chain_telemetry(void) +{ + const char *value = getenv("OAI_TX_CHAIN_TELEMETRY"); + tx_chain_telemetry = value != NULL && strcmp(value, "1") == 0; +} + +static bool tx_chain_telemetry_enabled(void) +{ + pthread_once(&tx_chain_telemetry_once, configure_tx_chain_telemetry); + return tx_chain_telemetry; +} + +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 +193,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 +241,48 @@ 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"); } + // One-shot proof that an 8TX run exercises every physical frequency-domain + // chain. Check each generated symbol chunk until its chain first carries + // energy; normal runs leave this disabled to avoid counting overhead. + if (tx_chain_telemetry_enabled()) { + const int sample_count = numSymbols * fp->ofdm_symbol_size; + const int sample_offset = startSymbol * fp->ofdm_symbol_size; + const c16_t *dst = (const c16_t *)&ru->common.txdataF_BF[tx_idx][sample_offset]; + const int nnz = count_nnz_c16(dst, sample_count); + if (nnz > 0) { + const unsigned bit = 1u << aa; + const unsigned seen = atomic_fetch_or_explicit( + &tx_chain_proven_mask, bit, memory_order_relaxed); + if ((seen & bit) == 0) + LOG_I(PHY, + "[TX_CHAIN_PROOF] frame=%d slot=%d ant=%d active=1 nnz=%d\n", + ru->proc.frame_tx, + slot, + aa, + nnz); + } + } + if (tx_idx == 0) stop_meas(&ru->precoding_stats); diff --git a/openair1/SCHED_NR/phy_procedures_nr_gNB.c b/openair1/SCHED_NR/phy_procedures_nr_gNB.c index 3a39240ea7..616454b9f3 100644 --- a/openair1/SCHED_NR/phy_procedures_nr_gNB.c +++ b/openair1/SCHED_NR/phy_procedures_nr_gNB.c @@ -270,7 +270,16 @@ void phy_procedures_gNB_TX(PHY_VARS_gNB *gNB, // reuse dlsch variables, as there are multiple very large memory // buffers gNB->dlsch[num_pdsch].pdsch_pdu = &dl_tti_pdu->pdsch_pdu; - gNB->dlsch[num_pdsch].pdu = (uint8_t *)TX_req->pdu_list[tx_data_idx].TLVs[0].value.direct; + const nfapi_nr_dl_tti_pdsch_pdu_rel15_t *pdsch = &dl_tti_pdu->pdsch_pdu.pdsch_pdu_rel15; + const nfapi_nr_pdu_t *tx_pdu = &TX_req->pdu_list[tx_data_idx]; + AssertFatal(tx_pdu->num_TLV == pdsch->NrOfCodewords, + "%4d.%2d PDSCH has %u codewords but TX request has %u TLVs\n", + frame, + slot, + pdsch->NrOfCodewords, + tx_pdu->num_TLV); + for (int q = 0; q < NR_MAX_NB_CODEWORDS; q++) + gNB->dlsch[num_pdsch].pdu[q] = q < tx_pdu->num_TLV ? (uint8_t *)tx_pdu->TLVs[q].value.direct : NULL; DevAssert(num_pdsch < gNB->max_nb_pdsch); num_pdsch++; } else { diff --git a/openair1/SCHED_NR_UE/phy_procedures_nr_ue.c b/openair1/SCHED_NR_UE/phy_procedures_nr_ue.c index c0aaf2978b..18b02cf0ff 100644 --- a/openair1/SCHED_NR_UE/phy_procedures_nr_ue.c +++ b/openair1/SCHED_NR_UE/phy_procedures_nr_ue.c @@ -499,9 +499,19 @@ static int nr_ue_pdsch_procedures(PHY_VARS_NR_UE *ue, int frame_rx = proc->frame_rx; int nr_slot_rx = proc->nr_slot_rx; - // We handle only one CW now - if (NR_MAX_NB_LAYERS > 4) { - LOG_E(NR_PHY, "Handling of more than one CW not implemented\n"); + if (dlsch->cw_info.Nl < 1 || dlsch->cw_info.Nl > NR_MAX_NB_LAYERS) { + LOG_E(NR_PHY, "Invalid PDSCH layer count %u\n", dlsch->cw_info.Nl); + return -1; + } + if (dlsch->cw_info.Nl > ue->frame_parms.nb_antennas_rx) { + LOG_E(NR_PHY, + "Cannot detect rank-%u PDSCH with only %u UE RX antennas\n", + dlsch->cw_info.Nl, + ue->frame_parms.nb_antennas_rx); + return -1; + } + if (dlsch->cw_info.Nl > 4 && (dlschCfg->pduBitmap & 0x1)) { + LOG_E(NR_PHY, "PTRS is not supported by the rank-5--8 joint PDSCH detector\n"); return -1; } @@ -582,14 +592,23 @@ static int nr_ue_pdsch_procedures(PHY_VARS_NR_UE *ue, nvar /= (dlschCfg->number_symbols * dlsch->cw_info.Nl * ue->frame_parms.nb_antennas_rx); nr_ue_measurement_procedures(2, ue, proc, freq_alloc->num_rbs, pdsch_est_size, pdsch_dl_ch_estimates); - if (ue->chest_time == 1) { // averaging time domain channel estimates - nr_chest_time_domain_avg(&ue->frame_parms, - (int32_t **)pdsch_dl_ch_estimates, - dlschCfg->number_symbols, - dlschCfg->start_symbol, - dlschCfg->dlDmrsSymbPos, - freq_alloc->num_rbs, - dlsch->cw_info.Nl); + if (ue->chest_time == 1 || dlsch->cw_info.Nl > 4) { + /* Rank 5--8 uses maxLength=len2 DMRS. Ports 1004--1007 share + * frequency-domain OCC with ports 1000--1003 and are separated by the + * time-domain OCC across the two front-loaded symbols, so averaging is + * mandatory. Apply it to every layer, not only the first RX block. */ + for (int layer = 0; layer < dlsch->cw_info.Nl; layer++) { + int32_t *layer_ch_estimates[ue->frame_parms.nb_antennas_rx]; + for (int aarx = 0; aarx < ue->frame_parms.nb_antennas_rx; aarx++) + layer_ch_estimates[aarx] = pdsch_dl_ch_estimates[layer * ue->frame_parms.nb_antennas_rx + aarx]; + + nr_chest_time_domain_avg(&ue->frame_parms, + layer_ch_estimates, + dlschCfg->number_symbols, + dlschCfg->start_symbol, + dlschCfg->dlDmrsSymbPos, + freq_alloc->num_rbs); + } } uint16_t first_symbol_with_data = dlschCfg->start_symbol; @@ -652,6 +671,7 @@ static int nr_ue_pdsch_procedures(PHY_VARS_NR_UE *ue, ue->frame_parms.nb_antennas_rx, rx_size_symbol, false); + int pdsch_status = 0; for (int m = dlschCfg->start_symbol; m < (dlschCfg->number_symbols + dlschCfg->start_symbol); m++) { bool first_symbol_flag = false; if (m == first_symbol_with_data) @@ -686,13 +706,8 @@ static int nr_ue_pdsch_procedures(PHY_VARS_NR_UE *ue, nvar, &scope_req) < 0) { - if (scope_req.copy_chanest_to_scope) { - UEunlockScopeData(ue, pdschChanEstimates); - } - if (scope_req.copy_rxdataF_to_scope) { - UEunlockScopeData(ue, pdschRxdataF); - } - return -1; + pdsch_status = -1; + break; } } // CRNTI active stop_meas_nr_ue_phy(ue, RX_PDSCH_STATS); @@ -707,7 +722,7 @@ static int nr_ue_pdsch_procedures(PHY_VARS_NR_UE *ue, free(toFree3); free(toFree4); free(toFree5); - return 0; + return pdsch_status; } static uint32_t compute_csi_rm_unav_res(fapi_nr_dl_config_dlsch_pdu_rel15_t *dlsch_config) @@ -797,7 +812,7 @@ static void nr_ue_dlsch_procedures(PHY_VARS_NR_UE *ue, } start_meas_nr_ue_phy(ue, DLSCH_UNSCRAMBLING_STATS); - nr_dlsch_unscrambling(llr, G, 0, config->dlDataScramblingId, dlsch->rnti); + nr_dlsch_unscrambling(llr, G, cw_idx, config->dlDataScramblingId, dlsch->rnti); stop_meas_nr_ue_phy(ue, DLSCH_UNSCRAMBLING_STATS); start_meas_nr_ue_phy(ue, DLSCH_DECODING_STATS); @@ -854,7 +869,7 @@ static void nr_ue_dlsch_procedures(PHY_VARS_NR_UE *ue, uint32_t dlsch_bytes = a_segments * 1056; // allocated bytes per segment if (ue->phy_sim_dlsch_b) - memcpy(ue->phy_sim_dlsch_b, dl_harq->b, dlsch_bytes); + memcpy(ue->phy_sim_dlsch_b + cw_idx * dlsch_bytes, dl_harq->b, dlsch_bytes); } static bool check_meas_to_perform(PHY_VARS_NR_UE *ue, int nr_slot_rx) @@ -1157,13 +1172,16 @@ void pdsch_processing(PHY_VARS_NR_UE *ue, const UE_nr_rxtx_proc_t *proc, nr_phy_ nr_ue_csi_rs_procedures(ue, proc, rxdataF, &phy_data->csirs_vars.csirs_config_pdu); } - int16_t *llr[2]; + int16_t *llr[NR_MAX_NB_CODEWORDS] = {0}; fapi_nr_dl_config_dlsch_pdu_rel15_t *dlsch_config = &phy_data->dlsch_config; - for (int c = 0; c < phy_data->n_dlsch_codewords; c++) { - NR_UE_DLSCH_t *dlsch = &phy_data->dlsch[c]; - if (!dlsch->active) - continue; + AssertFatal(phy_data->n_dlsch_codewords >= 0 && phy_data->n_dlsch_codewords <= NR_MAX_NB_CODEWORDS, + "Invalid number of PDSCH codewords %d\n", + phy_data->n_dlsch_codewords); + bool has_active_dlsch = false; + for (int c = 0; c < phy_data->n_dlsch_codewords; c++) + has_active_dlsch |= phy_data->dlsch[c].active; + if (has_active_dlsch) { uint16_t nb_symb_sch = dlsch_config->number_symbols; uint16_t start_symb_sch = dlsch_config->start_symbol; @@ -1198,52 +1216,127 @@ void pdsch_processing(PHY_VARS_NR_UE *ue, const UE_nr_rxtx_proc_t *proc, nr_phy_ unav_res = n_ptrs * ptrsSymbPerSlot; } unav_res += compute_csi_rm_unav_res(dlsch_config); - int G = nr_get_G(freq_alloc.num_rbs, - dlsch_config->number_symbols, - nb_re_dmrs, - dmrs_len, - unav_res, - dlsch->cw_info.qamModOrder, - dlsch->cw_info.Nl); - const uint32_t rx_llr_buf_sz = ALIGNARRAYSIZE(G, 32); // each LLR is 2 bytes hence 64 byte aligned - llr[c] = (int16_t *)malloc16_clear(rx_llr_buf_sz * sizeof(int16_t)); - - // dlsch_harq contains the previous transmissions data for this harq pid - NR_DL_UE_HARQ_t *harq = &ue->dl_harq_processes[c][dlsch_config->harq_process_nbr]; - // it returns -1 in case of internal failure, or 0 in case of normal result - int ret_pdsch = nr_ue_pdsch_procedures(ue, proc, dlsch, harq, dlsch_config, llr[c], rxdataF, &freq_alloc); - TracyCPlot("pdsch mcs", dlsch->cw_info.mcs); - - UEscopeCopy(ue, pdschLlr, llr[c], sizeof(int16_t), 1, G, 0); - - LOG_D(PHY, "DLSCH data reception at nr_slot_rx: %d\n", nr_slot_rx); - start_meas_nr_ue_phy(ue, DLSCH_PROCEDURES_STATS); - - if (ret_pdsch >= 0) { - nr_ue_dlsch_procedures(ue, proc, dlsch, c, G, &freq_alloc, dlsch_config, llr[c]); - } else { - LOG_E(NR_PHY, "Demodulation impossible, internal error\n"); - if (dlsch_config->k1_feedback) { - const int ack_nack_slot_and_frame = - proc->nr_slot_rx + dlsch_config->k1_feedback + proc->frame_rx * ue->frame_parms.slots_per_frame; - dynamic_barrier_join(&ue->process_slot_tx_barriers[ack_nack_slot_and_frame % NUM_PROCESS_SLOT_TX_BARRIERS]); - } - LOG_W(NR_PHY, "nr_ue_pdsch_procedures failed in slot %d\n", proc->nr_slot_rx); + int G[NR_MAX_NB_CODEWORDS] = {0}; + uint32_t rx_llr_buf_sz[NR_MAX_NB_CODEWORDS] = {0}; + for (int c = 0; c < phy_data->n_dlsch_codewords; c++) { + NR_UE_DLSCH_t *dlsch = &phy_data->dlsch[c]; + G[c] = nr_get_G(freq_alloc.num_rbs, + dlsch_config->number_symbols, + nb_re_dmrs, + dmrs_len, + unav_res, + dlsch->cw_info.qamModOrder, + dlsch->cw_info.Nl); + rx_llr_buf_sz[c] = ALIGNARRAYSIZE(G[c], 32); // each LLR is 2 bytes hence 64 byte aligned + llr[c] = (int16_t *)malloc16_clear(rx_llr_buf_sz[c] * sizeof(int16_t)); } - stop_meas_nr_ue_phy(ue, DLSCH_PROCEDURES_STATS); - if (cpumeas(CPUMEAS_GETSTATE)) { - LOG_D(PHY, "[SFN %d] Slot0 Slot1: Dlsch Proc %5.2f\n",nr_slot_rx,ue->phy_cpu_stats.cpu_time_stats[DLSCH_PROCEDURES_STATS].p_time/(cpuf*1000.0)); + int ret_pdsch = 0; + if (phy_data->n_dlsch_codewords == 2) { + NR_UE_DLSCH_t *cw0 = &phy_data->dlsch[0]; + NR_UE_DLSCH_t *cw1 = &phy_data->dlsch[1]; + const int total_layers = cw0->cw_info.Nl + cw1->cw_info.Nl; + if (total_layers <= 4 || total_layers > NR_MAX_NB_LAYERS) { + LOG_E(NR_PHY, "Two-codeword PDSCH has invalid aggregate rank %d\n", total_layers); + ret_pdsch = -1; + } else if (cw0->cw_info.qamModOrder != cw1->cw_info.qamModOrder) { + /* The joint detector is modulation-independent, but the current slot + * LLR layout uses one modulation order. The local gNB scheduler sends + * the same MCS on both TBs; reject unsupported external FAPI input + * explicitly rather than silently corrupting CW1. */ + LOG_E(NR_PHY, + "Rank-%d PDSCH with different modulation orders per codeword (%u/%u) is not supported\n", + total_layers, + cw0->cw_info.qamModOrder, + cw1->cw_info.qamModOrder); + ret_pdsch = -1; + } else { + NR_UE_DLSCH_t joint_dlsch = *cw0; + joint_dlsch.active = true; + joint_dlsch.cw_info.Nl = total_layers; + const int joint_G = G[0] + G[1]; + const uint32_t joint_buf_sz = ALIGNARRAYSIZE(joint_G, 32); + int16_t *joint_llr = (int16_t *)malloc16_clear(joint_buf_sz * sizeof(*joint_llr)); + NR_DL_UE_HARQ_t joint_harq = ue->dl_harq_processes[0][dlsch_config->harq_process_nbr]; + joint_harq.status = NR_ACTIVE; + ret_pdsch = nr_ue_pdsch_procedures( + ue, proc, &joint_dlsch, &joint_harq, dlsch_config, joint_llr, rxdataF, joint_G, &freq_alloc); + if (ret_pdsch >= 0) { + const int qm = cw0->cw_info.qamModOrder; + const int joint_stride = total_layers * qm; + AssertFatal(joint_G % joint_stride == 0, + "Rank-%d PDSCH G=%d is not divisible by the layer stride %d\n", + total_layers, + joint_G, + joint_stride); + int cw_offset[NR_MAX_NB_CODEWORDS] = {0}; + for (int re = 0; re < joint_G / joint_stride; re++) { + const int16_t *joint_re = joint_llr + re * joint_stride; + const int cw0_bits = cw0->cw_info.Nl * qm; + const int cw1_bits = cw1->cw_info.Nl * qm; + memcpy(llr[0] + cw_offset[0], joint_re, cw0_bits * sizeof(*joint_re)); + memcpy(llr[1] + cw_offset[1], joint_re + cw0_bits, cw1_bits * sizeof(*joint_re)); + cw_offset[0] += cw0_bits; + cw_offset[1] += cw1_bits; + } + AssertFatal(cw_offset[0] == G[0] && cw_offset[1] == G[1], + "Dual-codeword LLR split mismatch: %d/%d expected %d/%d\n", + cw_offset[0], + cw_offset[1], + G[0], + G[1]); + } + free(joint_llr); + } + } else { + NR_UE_DLSCH_t *dlsch = &phy_data->dlsch[0]; + NR_DL_UE_HARQ_t *harq = &ue->dl_harq_processes[0][dlsch_config->harq_process_nbr]; + ret_pdsch = nr_ue_pdsch_procedures(ue, proc, dlsch, harq, dlsch_config, llr[0], rxdataF, G[0], &freq_alloc); + } + + for (int c = 0; c < phy_data->n_dlsch_codewords; c++) { + NR_UE_DLSCH_t *dlsch = &phy_data->dlsch[c]; + TracyCPlot("pdsch mcs", dlsch->cw_info.mcs); + if (dlsch->active) + UEscopeCopy(ue, pdschLlr, llr[c], sizeof(int16_t), 1, G[c], 0); + + LOG_D(PHY, "DLSCH CW%d data reception at nr_slot_rx: %d\n", c, nr_slot_rx); + start_meas_nr_ue_phy(ue, DLSCH_PROCEDURES_STATS); + if (ret_pdsch >= 0) { + if (dlsch->active) + nr_ue_dlsch_procedures(ue, proc, dlsch, c, G[c], &freq_alloc, dlsch_config, llr[c]); + } else if (dlsch->active) { + LOG_E(NR_PHY, "PDSCH demodulation failed for CW%d\n", c); + if (dlsch_config->k1_feedback) { + const int ack_nack_slot_and_frame = + proc->nr_slot_rx + dlsch_config->k1_feedback + proc->frame_rx * ue->frame_parms.slots_per_frame; + dynamic_barrier_join(&ue->process_slot_tx_barriers[ack_nack_slot_and_frame % NUM_PROCESS_SLOT_TX_BARRIERS]); + } + LOG_W(NR_PHY, "nr_ue_pdsch_procedures failed in slot %d\n", proc->nr_slot_rx); + } + stop_meas_nr_ue_phy(ue, DLSCH_PROCEDURES_STATS); + if (cpumeas(CPUMEAS_GETSTATE)) + LOG_D(PHY, + "[SFN %d] Slot0 Slot1: Dlsch Proc %5.2f\n", + nr_slot_rx, + ue->phy_cpu_stats.cpu_time_stats[DLSCH_PROCEDURES_STATS].p_time / (cpuf * 1000.0)); } if (ue->phy_sim_rxdataF) memcpy(ue->phy_sim_rxdataF + start_symb_sch * ue->frame_parms.ofdm_symbol_size * sizeof(c16_t), &rxdataF[0][start_symb_sch * ue->frame_parms.ofdm_symbol_size], sizeof(int32_t) * nb_symb_sch * ue->frame_parms.ofdm_symbol_size); - if (ue->phy_sim_pdsch_llr) - memcpy(ue->phy_sim_pdsch_llr, llr[c], sizeof(int16_t) * rx_llr_buf_sz); + if (ue->phy_sim_pdsch_llr) { + int16_t *sim_llr = ue->phy_sim_pdsch_llr; + size_t llr_offset = 0; + for (int c = 0; c < phy_data->n_dlsch_codewords; c++) { + memcpy(sim_llr + llr_offset, llr[c], sizeof(*sim_llr) * G[c]); + llr_offset += G[c]; + } + } - free(llr[c]); + for (int c = 0; c < phy_data->n_dlsch_codewords; c++) + free(llr[c]); } if (nr_slot_rx==9) { diff --git a/openair1/SIMULATION/NR_PHY/dlschsim.c b/openair1/SIMULATION/NR_PHY/dlschsim.c index 2fabb6f267..ad3052bb9c 100644 --- a/openair1/SIMULATION/NR_PHY/dlschsim.c +++ b/openair1/SIMULATION/NR_PHY/dlschsim.c @@ -475,7 +475,7 @@ int main(int argc, char **argv) printf("harq process ue mcs = %d Qm = %d, symb %d\n", dlsch_config.cw_info[0].mcs, dlsch_config.cw_info[0].qamModOrder, nb_symb_sch); uint8_t test_input[TBS / 8 + 4]; // + 3 for CRC + 1 additional byte, see nr_dlsch_encoding() - dlsch->pdu = test_input; + dlsch->pdu[0] = test_input; //unsigned char test_input[TBS / 8] __attribute__ ((aligned(16))); for (i = 0; i < TBS / 8; i++) test_input[i] = (unsigned char) rand(); diff --git a/openair1/SIMULATION/NR_PHY/dlsim.c b/openair1/SIMULATION/NR_PHY/dlsim.c index dd73881fb1..13134a4102 100644 --- a/openair1/SIMULATION/NR_PHY/dlsim.c +++ b/openair1/SIMULATION/NR_PHY/dlsim.c @@ -240,14 +240,7 @@ void nr_dlsim_preprocessor(gNB_MAC_INST *nr_mac, post_process_pdsch_t *pp_pdsch) sched_pdsch.Qm = nr_get_Qm_dl(sched_pdsch.mcs, current_BWP->mcsTableIdx); sched_pdsch.R = nr_get_code_rate_dl(sched_pdsch.mcs, current_BWP->mcsTableIdx); - sched_pdsch.tb_size = nr_compute_tbs(sched_pdsch.Qm, - sched_pdsch.R, - sched_pdsch.rbSize, - sched_pdsch.tda_info.nrOfSymbols, - sched_pdsch.dmrs_parms.N_PRB_DMRS * sched_pdsch.dmrs_parms.N_DMRS_SLOT, - 0 /* N_PRB_oh, 0 for initialBWP */, - 0 /* tb_scaling */, - sched_pdsch.nrOfLayers) >> 3; + update_sched_pdsch_codeword_tbs(&sched_pdsch); /* the simulator assumes the HARQ PID is equal to the slot number */ sched_pdsch.dl_harq_pid = pp_pdsch->slot; @@ -471,7 +464,7 @@ int main(int argc, char **argv) case 'x': g_nrOfLayers = atoi(optarg); - if ((g_nrOfLayers == 0) || (g_nrOfLayers > 4)) { + if ((g_nrOfLayers == 0) || (g_nrOfLayers > NR_MAX_NB_LAYERS)) { printf("Unsupported nr Of Layers %d\n", g_nrOfLayers); exit(-1); } @@ -490,7 +483,7 @@ int main(int argc, char **argv) case 'y': n_tx=atoi(optarg); - if ((n_tx==0) || (n_tx>4)) {//extend gNB to support n_tx = 4 + if ((n_tx == 0) || (n_tx > NR_MAX_NB_LAYERS)) { printf("Unsupported number of tx antennas %d\n",n_tx); exit(-1); } @@ -500,7 +493,7 @@ int main(int argc, char **argv) case 'z': n_rx=atoi(optarg); - if ((n_rx==0) || (n_rx>4)) {//extend UE to support n_tx = 4 + if ((n_rx == 0) || (n_rx > NR_MAX_NB_LAYERS)) { printf("Unsupported number of rx antennas %d\n",n_rx); exit(-1); } @@ -940,8 +933,6 @@ int main(int argc, char **argv) UE_mac->if_module = nr_ue_if_module_init(0); unsigned int available_bits=0; - unsigned char *estimated_output_bit=NULL; - unsigned char *test_input_bit=NULL; unsigned int errors_bit = 0; initFloatingCoresTpool(dlsch_threads, &nrUE_params.Tpool, false, "UE-tpool"); @@ -987,7 +978,7 @@ int main(int argc, char **argv) a_segments = (a_segments/273)+1; } uint32_t dlsch_bytes = a_segments*1056; // allocated bytes per segment - UE->phy_sim_dlsch_b = calloc(1, dlsch_bytes); + UE->phy_sim_dlsch_b = calloc(NR_MAX_NB_CODEWORDS, dlsch_bytes); // csv file if (filename_csv != NULL) { @@ -1069,17 +1060,19 @@ int main(int argc, char **argv) UE_proc.nr_slot_rx = slot; UE_proc.gNB_id = 0; - NR_UE_DLSCH_t *dlsch0 = &phy_data.dlsch[0]; - int harq_pid = slot; - NR_DL_UE_HARQ_t *UE_harq_process = &UE->dl_harq_processes[0][harq_pid]; - - UE_harq_process->decodeResult = false; + NR_DL_UE_HARQ_t *UE_harq_process[NR_MAX_NB_CODEWORDS]; round = 0; - UE_harq_process->DLround = round; - UE_harq_process->first_rx = 1; + for (int c = 0; c < NR_MAX_NB_CODEWORDS; c++) { + UE_harq_process[c] = &UE->dl_harq_processes[c][harq_pid]; + UE_harq_process[c]->decodeResult = false; + UE_harq_process[c]->DLround = round; + UE_harq_process[c]->first_rx = 1; + } + int n_codewords = 0; + bool all_codewords_decoded = false; - while (round < num_rounds && !UE_harq_process->decodeResult && !stop) { + while (round < num_rounds && !all_codewords_decoded && !stop) { reset_sched_response(Sched_INFO, frame, slot, 0, 0); clear_nr_nfapi_information(RC.nrmac[0], 0, frame, slot); UE_info->UE_sched_ctrl.harq_processes[harq_pid].ndi = !(trial&1); @@ -1302,59 +1295,97 @@ int main(int argc, char **argv) //---------------------- count errors ---------------------- //---------------------------------------------------------- - if (dlsch0->last_iteration_cnt >= dlsch0->max_ldpc_iterations) + n_codewords = phy_data.n_dlsch_codewords; + AssertFatal(n_codewords >= 1 && n_codewords <= NR_MAX_NB_CODEWORDS, + "Invalid number of decoded PDSCH codewords %d\n", + n_codewords); + bool block_error = false; + bool any_active_codeword = false; + all_codewords_decoded = true; + for (int c = 0; c < n_codewords; c++) { + NR_UE_DLSCH_t *dlsch = &phy_data.dlsch[c]; + any_active_codeword |= dlsch->active; + block_error |= dlsch->active && dlsch->last_iteration_cnt >= dlsch->max_ldpc_iterations; + all_codewords_decoded &= UE_harq_process[c]->decodeResult; + } + if (block_error) n_errors[round]++; - - if (dlsch0->active) + if (any_active_codeword) round_trials[round]++; int16_t *UE_llr = (int16_t*)UE->phy_sim_pdsch_llr; - TBS = phy_data.dlsch_config.cw_info[0].TBS; + TBS = 0; uint16_t length_dmrs = get_num_dmrs(phy_data.dlsch_config.dlDmrsSymbPos); uint16_t nb_rb = phy_data.dlsch_config.number_rbs; uint8_t nb_re_dmrs = phy_data.dlsch_config.dmrsConfigType == NFAPI_NR_DMRS_TYPE1 ? 6 * phy_data.dlsch_config.n_dmrs_cdm_groups : 4 * phy_data.dlsch_config.n_dmrs_cdm_groups; - uint8_t mod_order = phy_data.dlsch_config.cw_info[0].qamModOrder; uint8_t nb_symb_sch = phy_data.dlsch_config.number_symbols; uint32_t unav_res = ptrsSymbPerSlot * ptrsRePerSymb; - available_bits = nr_get_G(nb_rb, nb_symb_sch, nb_re_dmrs, length_dmrs, unav_res, mod_order, pdsch_pdu_rel15->nrOfLayers); + available_bits = 0; + for (int c = 0; c < n_codewords; c++) { + const fapi_nr_dl_cw_info_t *cw_info = &phy_data.dlsch_config.cw_info[c]; + TBS += cw_info->TBS; + available_bits += nr_get_G(nb_rb, + nb_symb_sch, + nb_re_dmrs, + length_dmrs, + unav_res, + cw_info->qamModOrder, + cw_info->Nl); + } if (pdu_bit_map & 0x1) { if (trial == 0 && round == 0) { - printf("[DLSIM][PTRS] Available bits are: %5u, removed PTRS bits are: %5u \n", available_bits, (ptrsSymbPerSlot * ptrsRePerSymb * pdsch_pdu_rel15->nrOfLayers * mod_order)); + printf("[DLSIM][PTRS] Available bits are: %5u, removed PTRS REs are: %5u \n", + available_bits, + ptrsSymbPerSlot * ptrsRePerSymb * pdsch_pdu_rel15->nrOfLayers); } } - for (i = 0; i < available_bits; i++) { - const uint8_t current_bit = (gNB_dlsch->f[i / 8] & (1 << (i & 7))) >> (i & 7); - if (((current_bit == 0) && (UE_llr[i] <= 0)) || ((current_bit == 1) && (UE_llr[i] >= 0))) { - if (errors_scrambling[round] == 0) { - LOG_D(PHY,"First bit in error in unscrambling = %d\n",i); + uint32_t llr_offset = 0; + for (int c = 0; c < n_codewords; c++) { + const fapi_nr_dl_cw_info_t *cw_info = &phy_data.dlsch_config.cw_info[c]; + const uint32_t codeword_bits = nr_get_G(nb_rb, + nb_symb_sch, + nb_re_dmrs, + length_dmrs, + unav_res, + cw_info->qamModOrder, + cw_info->Nl); + for (i = 0; i < codeword_bits; i++) { + const uint8_t current_bit = (gNB_dlsch->f[c][i / 8] & (1 << (i & 7))) >> (i & 7); + if (((current_bit == 0) && (UE_llr[llr_offset + i] <= 0)) + || ((current_bit == 1) && (UE_llr[llr_offset + i] >= 0))) { + if (errors_scrambling[round] == 0) + LOG_D(PHY, "First bit in error in unscrambling = CW%d bit %d\n", c, i); + errors_scrambling[round]++; } - errors_scrambling[round]++; } + llr_offset += codeword_bits; } + AssertFatal(llr_offset == available_bits, + "PDSCH LLR accounting mismatch: %u of %u bits\n", + llr_offset, + available_bits); //printf("dlsim round %d ends\n",round); round++; } // round - if (test_input_bit == NULL) { - test_input_bit = (unsigned char *)malloc16(8 * pdsch_pdu_rel15->TBSize[0]); - estimated_output_bit = (unsigned char *)malloc16(8 * pdsch_pdu_rel15->TBSize[0]); - } - for (i = 0; i < TBS; i++) { + for (int c = 0; c < n_codewords; c++) { + const uint32_t codeword_tbs = phy_data.dlsch_config.cw_info[c].TBS; + const uint8_t *decoded = UE->phy_sim_dlsch_b + c * dlsch_bytes; + for (i = 0; i < codeword_tbs; i++) { + const uint8_t estimated_output_bit = (decoded[i / 8] & (1 << (i & 7))) >> (i & 7); + const uint8_t test_input_bit = (gNB_dlsch->b[c][i / 8] & (1 << (i & 7))) >> (i & 7); - estimated_output_bit[i] = (UE->phy_sim_dlsch_b[i/8] & (1 << (i & 7))) >> (i & 7); - test_input_bit[i] = (gNB_dlsch->b[i / 8] & (1 << (i & 7))) >> (i & 7); // Further correct for multiple segments - - if (estimated_output_bit[i] != test_input_bit[i]) { - if(errors_bit == 0) - LOG_D(PHY,"First bit in error in decoding = %d\n",i); - errors_bit++; - } - + if (estimated_output_bit != test_input_bit) { + if (errors_bit == 0) + LOG_D(PHY, "First bit in error in decoding = CW%d bit %d\n", c, i); + errors_bit++; + } + } } //////////////////////////////////////////////////////////// @@ -1364,7 +1395,7 @@ int main(int argc, char **argv) printf("errors_bit = %u (trial %d)\n", errors_bit, trial); } roundStats += round; - if (UE_harq_process->decodeResult) + if (all_codewords_decoded) effRate += ((double)TBS) / round; } // noise trials @@ -1391,7 +1422,11 @@ int main(int argc, char **argv) printf("), Channel BER (%e", berStats[0]); for (int r = 1; r < num_rounds; r++) printf(",%e", berStats[r]); - printf(") Avg round %.2f, Eff Rate %.4f bits/slot, Eff Throughput %.2f, TBS %u bits/slot\n", roundStats, effRate, effRate / (8 * pdsch_pdu_rel15->TBSize[0]) * 100, 8 * pdsch_pdu_rel15->TBSize[0]); + printf(") Avg round %.2f, Eff Rate %.4f bits/slot, Eff Throughput %.2f, TBS %u bits/slot\n", + roundStats, + effRate, + effRate / TBS * 100, + TBS); printf("*****************************************\n"); printf("\n"); // writing to csv file @@ -1399,7 +1434,7 @@ int main(int argc, char **argv) fprintf(csv_file,"%f,%d/%d,",SNR,n_false_positive,n_trials); for (int r = 0; r < num_rounds; r++) fprintf(csv_file,"%d/%d,%u/%u,%f,%e,",n_errors[r], round_trials[r], errors_scrambling[r], available_bits * round_trials[r],blerStats[r],berStats[r]); - fprintf(csv_file,"%.2f,%.4f,%.2f,%u\n", roundStats, effRate, effRate / (8 * pdsch_pdu_rel15->TBSize[0]) * 100, 8 * pdsch_pdu_rel15->TBSize[0]); + fprintf(csv_file, "%.2f,%.4f,%.2f,%u\n", roundStats, effRate, effRate / TBS * 100, TBS); } if (print_perf==1) { printf("\ngNB TX function statistics (per %d us slot, NPRB %d, mcs %d, C %d, block %d)\n", @@ -1407,7 +1442,7 @@ int main(int argc, char **argv) g_rbSize, g_mcsIndex, UE->dl_harq_processes[0][slot].C, - 8 * pdsch_pdu_rel15->TBSize[0]); + TBS); printDistribution(&gNB->phy_proc_tx,table_tx,"PHY proc tx"); printStatIndent2(&gNB->dci_generation_stats, "DCI encoding time"); printStatIndent2(&gNB->dlsch_encoding_stats,"DLSCH encoding time"); @@ -1490,10 +1525,11 @@ int main(int argc, char **argv) LOG_M("chestF0.m", "chF0", UE->phy_sim_pdsch_dl_ch_estimates, frame_parms->ofdm_symbol_size * 14, dec, op_format); op_format = (op_format & (~1)); LOG_M("rxF_llr.m", "rxFllr", UE->phy_sim_pdsch_llr, available_bits, dec, op_format); + ret = n_errors[0] == 0 && n_false_positive == 0 ? 0 : 1; break; } - if (effRate >= (eff_tp_check * 8 * pdsch_pdu_rel15->TBSize[0])) { + if (effRate >= eff_tp_check * TBS) { printf("PDSCH test OK\n"); ret = 0; break; @@ -1530,8 +1566,6 @@ int main(int argc, char **argv) free(r_re); free(r_im); free(txdata); - free(test_input_bit); - free(estimated_output_bit); free(UE->phy_sim_rxdataF); free(UE->phy_sim_pdsch_llr); free(UE->phy_sim_pdsch_rxdataF_ext); diff --git a/openair1/SIMULATION/TOOLS/random_channel.c b/openair1/SIMULATION/TOOLS/random_channel.c index 23c11d945c..27deb83eaf 100644 --- a/openair1/SIMULATION/TOOLS/random_channel.c +++ b/openair1/SIMULATION/TOOLS/random_channel.c @@ -2277,6 +2277,14 @@ void init_channelmod(void) { int load_channellist(uint8_t nb_tx, uint8_t nb_rx, double sampling_rate, uint64_t center_freq, double channel_bandwidth) { + // nakaolab Fix 9: ensure RNG is initialized exactly once, before any + // random_channel call (TDL/EPA/Rayleigh need gaussZiggurat). Static guard + // prevents multiple init races on the global tableNor* arrays. + static bool nakaolab_rng_done = false; + if (!nakaolab_rng_done) { + randominit(); + nakaolab_rng_done = true; + } paramdef_t achannel_params[] = CHANNELMOD_MODEL_PARAMS_DESC; paramlist_def_t channel_list; memset(&channel_list,0,sizeof(paramlist_def_t)); diff --git a/openair2/LAYER2/NR_MAC_COMMON/nr_mac_common.c b/openair2/LAYER2/NR_MAC_COMMON/nr_mac_common.c index ea1b2b1dc5..632c6659f4 100644 --- a/openair2/LAYER2/NR_MAC_COMMON/nr_mac_common.c +++ b/openair2/LAYER2/NR_MAC_COMMON/nr_mac_common.c @@ -3034,7 +3034,10 @@ uint16_t nr_dci_size(const NR_UE_DL_BWP_t *DL_BWP, // TB2 long *maxCWperDCI = pdsch_Config ? pdsch_Config->maxNrofCodeWordsScheduledByDCI : NULL; if (maxCWperDCI && (*maxCWperDCI == NR_PDSCH_Config__maxNrofCodeWordsScheduledByDCI_n2)) { - size += 8; + dci_pdu->mcs2.nbits = 5; + dci_pdu->ndi2.nbits = 1; + dci_pdu->rv2.nbits = 2; + size += dci_pdu->mcs2.nbits + dci_pdu->ndi2.nbits + dci_pdu->rv2.nbits; } // HARQ process number – 5 bits if higher layer parameter harq-ProcessNumberSizeDCI-1-1 is configured; // otherwise 4 bits. Spec 38.212 Section 7.3.1.2.2 diff --git a/openair2/LAYER2/NR_MAC_gNB/config.c b/openair2/LAYER2/NR_MAC_gNB/config.c index 1d217b7c72..8e089a2b05 100644 --- a/openair2/LAYER2/NR_MAC_gNB/config.c +++ b/openair2/LAYER2/NR_MAC_gNB/config.c @@ -207,13 +207,16 @@ nfapi_nr_pm_list_t init_DL_MIMO_codebook(gNB_MAC_INST *gNB, nr_pdsch_AntennaPort int O2 = N2 > 1 ? 4 : 1; //Vertical beam oversampling (1 or 4) int O1 = num_antenna_ports > 2 ? 4 : 1; //Horizontal beam oversampling (1 or 4) - int max_mimo_layers = (num_antenna_ports < NR_MAX_NB_LAYERS) ? num_antenna_ports : NR_MAX_NB_LAYERS; - AssertFatal(max_mimo_layers <= 4, "Max number of layers supported is 4\n"); + const int max_mimo_layers = min(num_antenna_ports, NR_MAX_NB_LAYERS); + /* Type-I codebook generation below currently covers ranks 1--4. Rank + * 5--8 is an explicit identity-PMI path and therefore needs no generated + * codebook entries. */ + const int codebook_layers = min(max_mimo_layers, 4); AssertFatal(num_antenna_ports < 16, "Max number of antenna ports supported is currently 16\n"); int K1 = 0, K2 = 0; nfapi_nr_pm_list_t mat = {.num_pm_idx = 0}; - for (int i = 0; i < max_mimo_layers; i++) { + for (int i = 0; i < codebook_layers; i++) { get_K1_K2(N1, N2, &K1, &K2, i + 1); int i2_size = i == 0 ? 4 : 2; gNB->precoding_matrix_size[i] = i2_size * N1 * O1 * N2 * O2 * K1 * K2; @@ -274,7 +277,7 @@ nfapi_nr_pm_list_t init_DL_MIMO_codebook(gNB_MAC_INST *gNB, nr_pdsch_AntennaPort } int pmiq = 0; - for (int layers = 1; layers <= max_mimo_layers; layers++) + for (int layers = 1; layers <= codebook_layers; layers++) pmiq = precoding_weigths_generation(&mat, pmiq, layers, N1, N2, O1, O2, num_antenna_ports, theta_n, v_lm); return mat; diff --git a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_dlsch.c b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_dlsch.c index d2f3b764d7..451643972d 100644 --- a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_dlsch.c +++ b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_dlsch.c @@ -404,6 +404,37 @@ bwp_info_t get_pdsch_bwp_start_size(gNB_MAC_INST *nr_mac, NR_UE_info_t *UE) return bwp_info; } +static uint32_t compute_sched_pdsch_tbs(const NR_sched_pdsch_t *sched, uint16_t rb_size, uint8_t codeword) +{ + const int cw_layers = nr_pdsch_layers_per_codeword(sched->nrOfLayers, codeword); + return nr_compute_tbs(sched->Qm, + sched->R, + rb_size, + sched->tda_info.nrOfSymbols, + sched->dmrs_parms.N_PRB_DMRS * sched->dmrs_parms.N_DMRS_SLOT, + 0, + 0, + cw_layers) + >> 3; +} + +void update_sched_pdsch_codeword_tbs(NR_sched_pdsch_t *sched) +{ + sched->tb_size = compute_sched_pdsch_tbs(sched, sched->rbSize, 0); + sched->tb_size2 = nr_pdsch_num_codewords(sched->nrOfLayers) == 2 ? compute_sched_pdsch_tbs(sched, sched->rbSize, 1) : 0; +} + +static void fit_dual_codeword_tbs(NR_sched_pdsch_t *sched, uint32_t requested_bytes, uint16_t max_rb_size) +{ + if (nr_pdsch_num_codewords(sched->nrOfLayers) == 1) + return; + update_sched_pdsch_codeword_tbs(sched); + while ((uint64_t)sched->tb_size + sched->tb_size2 < requested_bytes && sched->rbSize < max_rb_size) { + sched->rbSize++; + update_sched_pdsch_codeword_tbs(sched); + } +} + static bool allocate_dl_retransmission(gNB_MAC_INST *nr_mac, post_process_pdsch_t *pp_pdsch, int *n_rb_sched, @@ -420,7 +451,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 @@ -473,26 +504,43 @@ static bool allocate_dl_retransmission(gNB_MAC_INST *nr_mac, * that we have enough resources */ NR_pdsch_dmrs_t temp_dmrs = get_dl_dmrs_params(scc, dl_bwp, &temp_tda, layers); - uint32_t new_tbs; - uint16_t new_rbSize; - bool success = nr_find_nb_rb(new_sched.Qm, - new_sched.R, - 1, // no transform precoding for DL - layers, - temp_tda.nrOfSymbols, - temp_dmrs.N_PRB_DMRS * temp_dmrs.N_DMRS_SLOT, - new_sched.tb_size, - 1, /* minimum of 1RB: need to find exact TBS, don't preclude any number */ - bwp_size, - &new_tbs, - &new_rbSize); + uint16_t new_rbSize = 0; + if (nr_pdsch_num_codewords(layers) == 1) { + uint32_t new_tbs = 0; + bool success = nr_find_nb_rb(new_sched.Qm, + new_sched.R, + 1, // no transform precoding for DL + layers, + temp_tda.nrOfSymbols, + temp_dmrs.N_PRB_DMRS * temp_dmrs.N_DMRS_SLOT, + new_sched.tb_size, + 1, /* find an exact retransmission TBS */ + bwp_size, + &new_tbs, + &new_rbSize); + if (!success || new_tbs != new_sched.tb_size) + new_rbSize = 0; + } else { + NR_sched_pdsch_t candidate = new_sched; + candidate.nrOfLayers = layers; + candidate.dmrs_parms = temp_dmrs; + candidate.tda_info = temp_tda; + for (uint16_t rb = 1; rb <= bwp_size; rb++) { + candidate.rbSize = rb; + update_sched_pdsch_codeword_tbs(&candidate); + if (candidate.tb_size == new_sched.tb_size && candidate.tb_size2 == new_sched.tb_size2) { + new_rbSize = rb; + break; + } + } + } - if (!success || new_tbs != new_sched.tb_size) { + if (new_rbSize == 0) { LOG_D(NR_MAC, "[UE %04x][%4d.%2d] allocation of DL retransmission failed: new TBS %d of new TDA does not match old TBS %d\n", UE->rnti, frame, slot, - new_tbs, + 0, new_sched.tb_size); return false; /* the maximum TBsize we might have is smaller than what we need */ } @@ -505,13 +553,13 @@ static bool allocate_dl_retransmission(gNB_MAC_INST *nr_mac, /* we can allocate it. Overwrite the time_domain_allocation, the number * of RBs, and the new TB size. The rest is done below */ - new_sched.tb_size = new_tbs; new_sched.rbSize = new_rbSize; new_sched.time_domain_allocation = tda; new_sched.nrOfLayers = layers; new_sched.pm_index = pm_index; new_sched.dmrs_parms = temp_dmrs; new_sched.tda_info = temp_tda; + update_sched_pdsch_codeword_tbs(&new_sched); } /* Find a free CCE */ @@ -538,6 +586,11 @@ static bool allocate_dl_retransmission(gNB_MAC_INST *nr_mac, if (!get_FeedbackDisabled(UE->sc_info.downlinkHARQ_FeedbackDisabled_r17, current_harq_pid)) { int r_pucch = nr_get_pucch_resource(sched_ctrl->coreset, ul_bwp->pucch_Config, CCEIndex); alloc = nr_acknack_scheduling(nr_mac, UE, frame, slot, UE->UE_beam_index, r_pucch, 0); + if (alloc >= 0 && nr_pdsch_num_codewords(new_sched.nrOfLayers) == 2) { + const int second_alloc = nr_acknack_scheduling(nr_mac, UE, frame, slot, UE->UE_beam_index, r_pucch, 0); + if (second_alloc != alloc) + alloc = -1; + } if (alloc < 0) { LOG_D(NR_MAC, "[UE %04x][%4d.%2d] could not find PUCCH for DL DCI retransmission\n", UE->rnti, frame, slot); return false; @@ -565,7 +618,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 +752,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, @@ -822,10 +874,17 @@ static void pf_dl(gNB_MAC_INST *mac, /* Find PUCCH occasion: if it fails, undo CCE allocation (undoing PUCCH * allocation after CCE alloc fail would be more complex) */ + const int l = get_dl_nrOfLayers(mac, sched_ctrl, dl_bwp->dci_format); int alloc = -1; if (!get_FeedbackDisabled(iterator->UE->sc_info.downlinkHARQ_FeedbackDisabled_r17, sched_ctrl->available_dl_harq.head)) { int r_pucch = nr_get_pucch_resource(sched_ctrl->coreset, ul_bwp->pucch_Config, CCEIndex); alloc = nr_acknack_scheduling(mac, iterator->UE, frame, slot, iterator->UE->UE_beam_index, r_pucch, 0); + if (alloc >= 0 && nr_pdsch_num_codewords(l) == 2) { + const int second_alloc = + nr_acknack_scheduling(mac, iterator->UE, frame, slot, iterator->UE->UE_beam_index, r_pucch, 0); + if (second_alloc != alloc) + alloc = -1; + } if (alloc < 0) { LOG_D(NR_MAC, "[UE %04x][%4d.%2d] could not find PUCCH for DL DCI\n", rnti, frame, slot); reset_beam_status(&mac->beam_info, frame, slot, iterator->UE->UE_beam_index, slots_per_frame, beam.new_beam); @@ -837,7 +896,6 @@ 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); NR_sched_pdsch_t sched_pdsch = { // rbSize below .rbStart = rbStart, @@ -877,6 +935,7 @@ static void pf_dl(gNB_MAC_INST *mac, max_rbSize, &sched_pdsch.tb_size, &sched_pdsch.rbSize); + fit_dual_codeword_tbs(&sched_pdsch, sched_ctrl->num_total_bytes + oh, max_rbSize); post_process_dlsch(mac, pp_pdsch, iterator->UE, &sched_pdsch); @@ -943,13 +1002,15 @@ nfapi_nr_dl_tti_pdsch_pdu_rel15_t *prepare_pdsch_pdu(nfapi_nr_dl_tti_request_pdu pdsch_pdu->SubcarrierSpacing = dl_bwp ? dl_bwp->scs : *scc->ssbSubcarrierSpacing; pdsch_pdu->CyclicPrefix = dl_bwp && dl_bwp->cyclicprefix ? *dl_bwp->cyclicprefix : 0; // Codeword information - pdsch_pdu->NrOfCodewords = 1; - pdsch_pdu->targetCodeRate[0] = sched_pdsch->R; - pdsch_pdu->qamModOrder[0] = sched_pdsch->Qm; - pdsch_pdu->mcsIndex[0] = sched_pdsch->mcs; - pdsch_pdu->mcsTable[0] = dl_bwp ? dl_bwp->mcsTableIdx : 0; - pdsch_pdu->rvIndex[0] = nr_get_rv(harq_round % 4); - pdsch_pdu->TBSize[0] = sched_pdsch->tb_size; + pdsch_pdu->NrOfCodewords = nr_pdsch_num_codewords(sched_pdsch->nrOfLayers); + for (int q = 0; q < pdsch_pdu->NrOfCodewords; q++) { + pdsch_pdu->targetCodeRate[q] = sched_pdsch->R; + pdsch_pdu->qamModOrder[q] = sched_pdsch->Qm; + pdsch_pdu->mcsIndex[q] = sched_pdsch->mcs; + pdsch_pdu->mcsTable[q] = dl_bwp ? dl_bwp->mcsTableIdx : 0; + pdsch_pdu->rvIndex[q] = nr_get_rv(harq_round % 4); + pdsch_pdu->TBSize[q] = q == 0 ? sched_pdsch->tb_size : sched_pdsch->tb_size2; + } pdsch_pdu->dataScramblingId = pdsch_Config && pdsch_Config->dataScramblingIdentityPDSCH ? *pdsch_Config->dataScramblingIdentityPDSCH : *scc->physCellId; pdsch_pdu->nrOfLayers = sched_pdsch->nrOfLayers; pdsch_pdu->transmissionScheme = 0; @@ -961,7 +1022,7 @@ nfapi_nr_dl_tti_pdsch_pdu_rel15_t *prepare_pdsch_pdu(nfapi_nr_dl_tti_request_pdu pdsch_pdu->SCID = dmrs_parms->n_scid; pdsch_pdu->dlDmrsScramblingId = dmrs_parms->scrambling_id; pdsch_pdu->numDmrsCdmGrpsNoData = dmrs_parms->numDmrsCdmGrpsNoData; - pdsch_pdu->dmrsPorts = (1 << sched_pdsch->nrOfLayers) - 1; // FIXME with a better implementation + pdsch_pdu->dmrsPorts = dmrs_parms->dmrs_ports; // Pdsch Allocation in frequency domain pdsch_pdu->resourceAlloc = 1; pdsch_pdu->rbStart = sched_pdsch->rbStart; @@ -998,6 +1059,98 @@ nfapi_nr_dl_tti_pdsch_pdu_rel15_t *prepare_pdsch_pdu(nfapi_nr_dl_tti_request_pdu return pdsch_pdu; } +typedef struct { + int sdus; + int sdu_bytes; +} dlsch_fill_result_t; + +static dlsch_fill_result_t fill_dlsch_transport_block(gNB_MAC_INST *mac, + NR_UE_info_t *UE, + uint8_t *tb, + uint32_t tbs, + frame_t frame, + slot_t slot, + bool include_ces) +{ + const int module_id = mac->Mod_id; + NR_UE_sched_ctrl_t *sched_ctrl = &UE->UE_sched_ctrl; + dlsch_fill_result_t result = {0}; + uint8_t *buf = tb; + + if (include_ces) + buf += nr_write_ce_dlsch_pdu(module_id, sched_ctrl, buf, 255, NULL); + DevAssert(buf < tb + tbs); + uint8_t *buf_end = tb + tbs; + + start_meas(&mac->rlc_data_req); + if (sched_ctrl->num_total_bytes > 0) { + for (int i = 0; i < seq_arr_size(&sched_ctrl->lc_config); ++i) { + const nr_lc_config_t *c = seq_arr_at(&sched_ctrl->lc_config, i); + const int lcid = c->lcid; + if (sched_ctrl->rlc_status[lcid].bytes_in_buffer == 0) + continue; + + tb_size_t pdu_siz[MAX_NUM_DATA_REQ]; + int num = nr_mac_rlc_multi_data_req(module_id, + UE->rnti, + true, + lcid, + buf_end - buf, + (char *)buf, + pdu_siz, + sizeofArray(pdu_siz)); + DevAssert(num <= sizeofArray(pdu_siz)); + result.sdus += num; + int lcid_bytes = 0; + for (int j = 0; j < num; ++j) { + NR_MAC_SUBHEADER_LONG *header = (NR_MAC_SUBHEADER_LONG *)buf; + header->R = 0; + header->F = 1; + header->LCID = lcid; + header->L = htons(pdu_siz[j]); + buf += pdu_siz[j] + sizeof(NR_MAC_SUBHEADER_LONG); + result.sdu_bytes += pdu_siz[j]; + lcid_bytes += pdu_siz[j]; + } + T(T_GNB_MAC_LCID_DL, + T_INT(UE->rnti), + T_INT(frame), + T_INT(slot), + T_INT(lcid), + T_INT(lcid_bytes), + T_INT(nr_rlc_tx_list_occupancy(UE->rnti, lcid))); + UE->mac_stats.dl.lc_bytes[lcid] += lcid_bytes; + } + } else if (get_softmodem_params()->phy_test || get_softmodem_params()->do_ra) { + LOG_D(NR_MAC, "Configuring DL_TX in %d.%d: TBS %d of random data\n", frame, slot, tbs); + if (buf_end - buf > sizeof(NR_MAC_SUBHEADER_LONG)) { + NR_MAC_SUBHEADER_LONG *header = (NR_MAC_SUBHEADER_LONG *)buf; + header->R = 0; + header->F = 1; + header->LCID = DL_SCH_LCID_PADDING; + buf += sizeof(NR_MAC_SUBHEADER_LONG); + header->L = htons(buf_end - buf); + for (; ((intptr_t)buf) % 4; buf++) + *buf = lrand48() & 0xff; + for (; buf < buf_end - 3; buf += 4) + *(uint32_t *)buf = lrand48(); + for (; buf < buf_end; buf++) + *buf = lrand48() & 0xff; + result.sdus++; + } + } + stop_meas(&mac->rlc_data_req); + + if (buf_end - buf > 0) { + NR_MAC_SUBHEADER_FIXED *padding = (NR_MAC_SUBHEADER_FIXED *)buf; + padding->R = 0; + padding->LCID = DL_SCH_LCID_PADDING; + buf++; + memset(buf, 0, buf_end - buf); + } + return result; +} + void post_process_dlsch(gNB_MAC_INST *nr_mac, post_process_pdsch_t *pdsch, NR_UE_info_t *UE, NR_sched_pdsch_t *sched_pdsch) { int CC_id = 0; @@ -1014,6 +1167,7 @@ void post_process_dlsch(gNB_MAC_INST *nr_mac, post_process_pdsch_t *pdsch, NR_UE /* POST processing */ const uint8_t nrOfLayers = sched_pdsch->nrOfLayers; const uint32_t TBS = sched_pdsch->tb_size; + const uint32_t TBS2 = sched_pdsch->tb_size2; int8_t current_harq_pid = sched_pdsch->dl_harq_pid; if (current_harq_pid < 0) { @@ -1208,11 +1362,15 @@ void post_process_dlsch(gNB_MAC_INST *nr_mac, post_process_pdsch_t *pdsch, NR_UE harq->round, harq->ndi); if (!get_softmodem_params()->phy_test) - AssertFatal(harq->sched_pdsch.tb_size == TBS, - "UE %04x mismatch between scheduled TBS and buffered TB for HARQ PID %d\n", + AssertFatal(harq->sched_pdsch.tb_size == TBS && harq->sched_pdsch.tb_size2 == TBS2, + "UE %04x mismatch between scheduled TBS (%u,%u) and buffered TB (%u,%u) for HARQ PID %d\n", UE->rnti, + TBS, + TBS2, + harq->sched_pdsch.tb_size, + harq->sched_pdsch.tb_size2, current_harq_pid); - else if (harq->sched_pdsch.tb_size != TBS) + else if (harq->sched_pdsch.tb_size != TBS || harq->sched_pdsch.tb_size2 != TBS2) LOG_E(NR_MAC, "Mismatch between scheduled TBS and buffered TB for HARQ PID %d. No RTX control in phy-test mode. " "Possible causes: presence of CSI-RS or DLSCH scheduled in the mixed slot.\n", @@ -1225,94 +1383,24 @@ void post_process_dlsch(gNB_MAC_INST *nr_mac, post_process_pdsch_t *pdsch, NR_UE LOG_D(NR_MAC, "Initial HARQ transmission in %d.%d\n", frame, slot); // Flag HARQ process to start TCI timer at ACK harq->start_tci_timer = sched_ctrl->UE_mac_ce_ctrl.tci_state_ind.is_scheduled; - uint8_t *buf = allocate_transportBlock_buffer(&harq->transportBlock, TBS); - /* first, write all CEs that might be there */ - int written = nr_write_ce_dlsch_pdu(module_id, - sched_ctrl, - (unsigned char *)buf, - 255, // no drx - NULL); // contention res id - buf += written; - uint8_t *bufEnd = buf + TBS - written; - DevAssert(TBS > written); int dlsch_total_bytes = 0; - /* next, get RLC data */ - start_meas(&nr_mac->rlc_data_req); int sdus = 0; - - if (sched_ctrl->num_total_bytes > 0) { - /* loop over all activated logical channels */ - for (int i = 0; i < seq_arr_size(&sched_ctrl->lc_config); ++i) { - const nr_lc_config_t *c = seq_arr_at(&sched_ctrl->lc_config, i); - const int lcid = c->lcid; - - if (sched_ctrl->rlc_status[lcid].bytes_in_buffer == 0) - continue; // no data for this LC tbs_size_t len = 0; - - - tb_size_t pdu_siz[MAX_NUM_DATA_REQ]; - int num = nr_mac_rlc_multi_data_req(module_id, rnti, true, lcid, bufEnd - buf, (char *)buf, pdu_siz, sizeofArray(pdu_siz)); - DevAssert(num <= sizeofArray(pdu_siz)); - - sdus += num; - int lcid_bytes = 0; - for (int j = 0; j < num; ++j) { - NR_MAC_SUBHEADER_LONG *header = (NR_MAC_SUBHEADER_LONG *) buf; - header->R = 0; - header->F = 1; - header->LCID = lcid; - header->L = htons(pdu_siz[j]); - buf += pdu_siz[j] + sizeof(NR_MAC_SUBHEADER_LONG); - dlsch_total_bytes += pdu_siz[j]; - lcid_bytes += pdu_siz[j]; - } - - T(T_GNB_MAC_LCID_DL, T_INT(rnti), T_INT(frame), T_INT(slot), T_INT(lcid), T_INT(lcid_bytes), T_INT(nr_rlc_tx_list_occupancy(rnti, lcid))); - UE->mac_stats.dl.lc_bytes[lcid] += lcid_bytes; - } - } else if (get_softmodem_params()->phy_test || get_softmodem_params()->do_ra) { - /* we will need the large header, phy-test typically allocates all - * resources and fills to the last byte below */ - LOG_D(NR_MAC, "Configuring DL_TX in %d.%d: TBS %d of random data\n", frame, slot, TBS); - - if (bufEnd-buf > sizeof(NR_MAC_SUBHEADER_LONG) ) { - NR_MAC_SUBHEADER_LONG *header = (NR_MAC_SUBHEADER_LONG *) buf; - // fill dlsch_buffer with random data - header->R = 0; - header->F = 1; - header->LCID = DL_SCH_LCID_PADDING; - buf += sizeof(NR_MAC_SUBHEADER_LONG); - header->L = htons(bufEnd-buf); - - for (; ((intptr_t)buf) % 4; buf++) - *buf = lrand48() & 0xff; - for (; buf < bufEnd - 3; buf += 4) { - uint32_t *buf32 = (uint32_t *)buf; - *buf32 = lrand48(); - } - for (; buf < bufEnd; buf++) - *buf = lrand48() & 0xff; - sdus +=1; - } + for (int q = 0; q < nr_pdsch_num_codewords(nrOfLayers); q++) { + const uint32_t cw_tbs = q == 0 ? TBS : sched_pdsch->tb_size2; + byte_array_t *transport_block = q == 0 ? &harq->transportBlock : &harq->transportBlock2; + uint8_t *buf = allocate_transportBlock_buffer(transport_block, cw_tbs); + dlsch_fill_result_t fill = fill_dlsch_transport_block(nr_mac, UE, buf, cw_tbs, frame, slot, q == 0); + sdus += fill.sdus; + dlsch_total_bytes += fill.sdu_bytes; } - stop_meas(&nr_mac->rlc_data_req); - - // Add padding header and zero rest out if there is space left - if (bufEnd-buf > 0) { - NR_MAC_SUBHEADER_FIXED *padding = (NR_MAC_SUBHEADER_FIXED *) buf; - padding->R = 0; - padding->LCID = DL_SCH_LCID_PADDING; - buf += 1; - memset(buf,0,bufEnd-buf); - buf=bufEnd; - } - - UE->mac_stats.dl.total_bytes += TBS; - UE->mac_stats.dl.current_bytes = TBS; + const uint32_t total_tbs = TBS + sched_pdsch->tb_size2; + UE->mac_stats.dl.total_bytes += total_tbs; + UE->mac_stats.dl.current_bytes = total_tbs; UE->mac_stats.dl.total_rbs += sched_pdsch->rbSize; UE->mac_stats.dl.num_mac_sdu += sdus; UE->mac_stats.dl.current_rbs = sched_pdsch->rbSize; + UE->mac_stats.dl.last_layers = sched_pdsch->nrOfLayers; UE->mac_stats.dl.total_sdu_bytes += dlsch_total_bytes; nr_mac->mac_stats.dl.used_prb_aggregate += sched_pdsch->rbSize; @@ -1349,10 +1437,16 @@ void post_process_dlsch(gNB_MAC_INST *nr_mac, post_process_pdsch_t *pdsch, NR_UE const int ntx_req = pdsch->TX_req->Number_of_PDUs; nfapi_nr_pdu_t *tx_req = &pdsch->TX_req->pdu_list[ntx_req]; tx_req->PDU_index = pduindex; - tx_req->num_TLV = 1; - tx_req->TLVs[0].length = TBS; - tx_req->PDU_length = compute_PDU_length(tx_req->num_TLV, tx_req->TLVs[0].length); - memcpy(tx_req->TLVs[0].value.direct, harq->transportBlock.buf, TBS); + tx_req->num_TLV = nr_pdsch_num_codewords(nrOfLayers); + uint32_t total_tlv_length = 0; + for (int q = 0; q < tx_req->num_TLV; q++) { + const uint32_t cw_tbs = q == 0 ? TBS : TBS2; + const byte_array_t *transport_block = q == 0 ? &harq->transportBlock : &harq->transportBlock2; + tx_req->TLVs[q].length = cw_tbs; + memcpy(tx_req->TLVs[q].value.direct, transport_block->buf, cw_tbs); + total_tlv_length += cw_tbs; + } + tx_req->PDU_length = compute_PDU_length(tx_req->num_TLV, total_tlv_length); pdsch->TX_req->Number_of_PDUs++; pdsch->TX_req->SFN = frame; pdsch->TX_req->Slot = slot; diff --git a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_phytest.c b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_phytest.c index 68a5d5799a..db8f5026d7 100644 --- a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_phytest.c +++ b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_phytest.c @@ -166,15 +166,7 @@ void nr_preprocessor_phytest(gNB_MAC_INST *mac, post_process_pdsch_t *pp_pdsch) .tda_info = tda_info, }; sched_ctrl->dl_bler_stats.mcs = target_dl_mcs; /* for logging output */ - sched_pdsch.tb_size = nr_compute_tbs(sched_pdsch.Qm, - sched_pdsch.R, - sched_pdsch.rbSize, - tda_info.nrOfSymbols, - sched_pdsch.dmrs_parms.N_PRB_DMRS * sched_pdsch.dmrs_parms.N_DMRS_SLOT, - 0 /* N_PRB_oh, 0 for initialBWP */, - 0 /* tb_scaling */, - target_dl_Nl) - >> 3; + update_sched_pdsch_codeword_tbs(&sched_pdsch); post_process_dlsch(mac, pp_pdsch, UE, &sched_pdsch); diff --git a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_primitives.c b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_primitives.c index 3dc39f9612..d7d5484d00 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 || layers > 4) + 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 @@ -304,6 +328,11 @@ NR_pdsch_dmrs_t get_dl_dmrs_params(const NR_ServingCellConfigCommon_t *scc, { NR_pdsch_dmrs_t dmrs = {0}; int frontloaded_symb = 1; // default value + NR_PDSCH_Config_t *pdsch_Config = dl_bwp ? dl_bwp->pdsch_Config : NULL; + NR_DMRS_DownlinkConfig_t *dmrs_Config = NULL; + if (pdsch_Config) + dmrs_Config = tda_info->mapping_type == typeB ? pdsch_Config->dmrs_DownlinkForPDSCH_MappingTypeB->choice.setup + : pdsch_Config->dmrs_DownlinkForPDSCH_MappingTypeA->choice.setup; nr_dci_format_t dci_format = dl_bwp ? dl_bwp->dci_format : NR_DL_DCI_FORMAT_1_0; if (dci_format == NR_DL_DCI_FORMAT_1_0) { dmrs.numDmrsCdmGrpsNoData = tda_info->nrOfSymbols <= 2 ? 1 : 2; @@ -338,19 +367,29 @@ NR_pdsch_dmrs_t get_dl_dmrs_params(const NR_ServingCellConfigCommon_t *scc, dmrs.numDmrsCdmGrpsNoData = 2; frontloaded_symb = 1; break; + case 5: + case 6: + case 7: + case 8: + AssertFatal(dmrs_Config && dmrs_Config->dmrs_Type == NULL && dmrs_Config->maxLength, + "Rank %d PDSCH requires DMRS type 1 with maxLength len2\n", + Layers); + /* TS 38.212 Table 7.3.1.2.2-2, two-codeword rows 0--3. */ + dmrs.dmrs_ports_id = Layers - 5; + dmrs.numDmrsCdmGrpsNoData = 2; + frontloaded_symb = 2; + break; default: AssertFatal(1==0,"Number of layers %d\n not supported or not valid\n",Layers); } } dmrs.n_scid = 0; + static const uint16_t two_cw_dmrs_ports[] = {0x1f, 0x5f, 0x7f, 0xff}; + dmrs.dmrs_ports = Layers > 4 ? two_cw_dmrs_ports[Layers - 5] : (1U << Layers) - 1; long *scramblingID = NULL; - NR_PDSCH_Config_t *pdsch_Config = dl_bwp ? dl_bwp->pdsch_Config : NULL; if (pdsch_Config) { - NR_DMRS_DownlinkConfig_t *dmrs_Config = tda_info->mapping_type == typeB ? - pdsch_Config->dmrs_DownlinkForPDSCH_MappingTypeB->choice.setup : - pdsch_Config->dmrs_DownlinkForPDSCH_MappingTypeA->choice.setup; - dmrs.phaseTrackingRS = dmrs_Config->phaseTrackingRS ? dmrs_Config->phaseTrackingRS->choice.setup : NULL; + dmrs.phaseTrackingRS = Layers > 4 ? NULL : (dmrs_Config->phaseTrackingRS ? dmrs_Config->phaseTrackingRS->choice.setup : NULL); dmrs.dmrsConfigType = dmrs_Config->dmrs_Type != NULL; scramblingID = dmrs.n_scid ? dmrs_Config->scramblingID1 : dmrs_Config->scramblingID0; } else { @@ -930,6 +969,11 @@ dci_pdu_rel15_t prepare_dci_dl_payload(const gNB_MAC_INST *gNB_mac, AssertFatal(harq, "HARQ process should be available for DCI with RNTI %s\n", rnti_types(rnti_type)); dci_payload.harq_pid.val = harq_pid; dci_payload.ndi = harq->ndi; + if (pdsch_pdu->NrOfCodewords == 2) { + dci_payload.mcs2.val = sched_pdsch->mcs; + dci_payload.ndi2.val = harq->ndi; + dci_payload.rv2.val = pdsch_pdu->rvIndex[1]; + } dci_payload.pucch_resource_indicator = pucch ? pucch->resource_indicator : 0; dci_payload.pdsch_to_harq_feedback_timing_indicator.val = pucch ? pucch->timing_indicator : 0; // PDSCH to HARQ TI dci_payload.dai[0].val = pucch ? (pucch->dai_c - 1) & 3 : 0; @@ -2518,8 +2562,10 @@ void delete_nr_ue_data(NR_UE_info_t *UE, uid_allocator_t *uia) destroy_nr_list(&sched_ctrl->available_ul_harq); destroy_nr_list(&sched_ctrl->feedback_ul_harq); destroy_nr_list(&sched_ctrl->retrans_ul_harq); - for (int i = 0; i < NR_MAX_HARQ_PROCESSES; ++i) + for (int i = 0; i < NR_MAX_HARQ_PROCESSES; ++i) { free_transportBlock_buffer(&sched_ctrl->harq_processes[i].transportBlock); + free_transportBlock_buffer(&sched_ctrl->harq_processes[i].transportBlock2); + } free_sched_pucch_list(sched_ctrl); uid_linear_allocator_free(uia, UE->uid); LOG_I(NR_MAC, "Remove NR rnti 0x%04x\n", UE->rnti); @@ -3614,9 +3660,16 @@ void nr_mac_update_timers(module_id_t module_id) NR_UE_sched_ctrl_t *sched_ctrl = &UE->UE_sched_ctrl; if (nr_mac_check_release(sched_ctrl)) { - // trigger release first as nr_mac_release_ue() invalidates UE ptr - nr_mac_trigger_release_complete(mac, UE->rnti); - nr_mac_release_ue(mac, UE->rnti); + // nakaolab UE release race fix: + // Save rnti before nr_mac_trigger_release_complete — that call drops + // the scheduler lock briefly (to send F1AP UE Context Release Complete), + // and another thread (e.g., RRC DL handler) can free this UE during + // that window, leaving UE->rnti pointing at freed memory. + rnti_t rnti = UE->rnti; + nr_mac_trigger_release_complete(mac, rnti); + // Re-validate after lock re-acquired: skip release if already gone. + if (find_nr_UE(&mac->UE_info, rnti) != NULL) + nr_mac_release_ue(mac, rnti); // go back to examine the next UE, which is at the position the // current UE was UE--; diff --git a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_uci.c b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_uci.c index d0ab235276..7840b3d974 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); @@ -716,6 +718,7 @@ static void extract_pucch_csi_report(NR_CSI_MeasConfig_t *csi_MeasConfig, int cumul_bits = 0; int r_index = -1; int new_bf_index = -1; + bool link_csi_decoded = false; for (int csi_report_id = 0; csi_report_id < csi_MeasConfig->csi_ReportConfigToAddModList->list.count; csi_report_id++) { nr_csi_report_t *csi_report = &UE->csi_report_template[csi_report_id]; csi_report->nb_of_csi_ssb_report = 0; @@ -761,12 +764,25 @@ 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); evaluate_cqi_report(payload, csi_report, cumul_bits, r_index, UE, cqi_table); + link_csi_decoded |= r_index >= 0; break; case NR_CSI_ReportConfig__reportQuantity_PR_cri_RI_PMI_CQI: sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.print_report = true; @@ -775,8 +791,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); @@ -784,6 +812,7 @@ static void extract_pucch_csi_report(NR_CSI_MeasConfig_t *csi_MeasConfig, sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.csi_report_id = csi_report_id; cumul_bits += pmi_bitlen; evaluate_cqi_report(payload, csi_report, cumul_bits, r_index, UE, cqi_table); + link_csi_decoded |= r_index >= 0; break; case NR_CSI_ReportConfig__reportQuantity_PR_cri_RI_LI_PMI_CQI: sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.print_report = true; @@ -792,8 +821,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; @@ -803,6 +844,7 @@ static void extract_pucch_csi_report(NR_CSI_MeasConfig_t *csi_MeasConfig, sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.csi_report_id = csi_report_id; cumul_bits += pmi_bitlen; evaluate_cqi_report(payload, csi_report, cumul_bits, r_index, UE, cqi_table); + link_csi_decoded |= r_index >= 0; break; default: AssertFatal(1 == 0, "Invalid or not supported CSI measurement report\n"); @@ -810,6 +852,10 @@ static void extract_pucch_csi_report(NR_CSI_MeasConfig_t *csi_MeasConfig, } } } + if (link_csi_decoded) { + sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.report_frame = frame; + sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.report_slot = slot; + } if ((new_bf_index !=-1) && !nrmac->radio_config.do_TCI) // Trigger RRCReconfiguration. Need to be out of the for loop as it may modify csi_MeasConfig beam_switching_procedure(nrmac, UE, new_bf_index); @@ -882,8 +928,7 @@ void handle_nr_uci_pucch_0_1(module_id_t mod_id, frame_t frame, slot_t slot, con NR_UE_sched_ctrl_t *sched_ctrl = &UE->UE_sched_ctrl; if (((uci_01->pduBitmap >> 1) & 0x01)) { // iterate over received harq bits - for (int harq_bit = 0; harq_bit < uci_01->harq.num_harq; harq_bit++) { - const uint8_t harq_value = uci_01->harq.harq_list[harq_bit].harq_value; + for (int harq_bit = 0; harq_bit < uci_01->harq.num_harq;) { const uint8_t harq_confidence = uci_01->harq.harq_confidence_level; NR_UE_harq_t *harq = find_harq(frame, slot, UE, nrmac->dl_bler.harq_round_max); if (!harq) { @@ -891,11 +936,16 @@ void handle_nr_uci_pucch_0_1(module_id_t mod_id, frame_t frame, slot_t slot, con break; } DevAssert(harq->is_waiting); + const int n_ack_bits = harq->sched_pdsch.nrOfLayers > 4 ? 2 : 1; const int8_t pid = sched_ctrl->feedback_dl_harq.head; remove_front_nr_list(&sched_ctrl->feedback_dl_harq); - LOG_D(NR_MAC,"%4d.%2d bit %d pid %d ack/nack %d\n",frame, slot, harq_bit,pid,harq_value); + bool success = harq_confidence == 0 && harq_bit + n_ack_bits <= uci_01->harq.num_harq; + for (int q = 0; q < n_ack_bits && harq_bit + q < uci_01->harq.num_harq; q++) { + const uint8_t harq_value = uci_01->harq.harq_list[harq_bit + q].harq_value; + LOG_D(NR_MAC, "%4d.%2d bit %d pid %d cw %d ack/nack %d\n", frame, slot, harq_bit + q, pid, q, harq_value); + success &= harq_value == 0; + } nr_mac_update_pdcch_closed_loop_adjust(sched_ctrl, harq_confidence != 0); - bool success = harq_value == 0 && harq_confidence == 0; // TCI state switch occurs at the first slot that is after slot n_+ T_HARQ + 3N_sf_slot (8.10.3 of 38.133) if (success && harq->start_tci_timer) { int slots = 3 * nrmac->frame_structure.numb_slots_frame / 10; @@ -919,6 +969,7 @@ void handle_nr_uci_pucch_0_1(module_id_t mod_id, frame_t frame, slot_t slot, con // DTX for each _bit_, but maybe once for PUCCH is enough? not sure nr_mac_signal_dtx(&sched_ctrl->pucch_pc); } + harq_bit += n_ack_bits; } // tpc (power control) only if we received AckNack @@ -984,19 +1035,24 @@ void handle_nr_uci_pucch_2_3_4(module_id_t mod_id, frame_t frame, slot_t slot, c if ((uci_234->pduBitmap >> 1) & 0x01) { // iterate over received harq bits - for (int harq_bit = 0; harq_bit < uci_234->harq.harq_bit_len; harq_bit++) { - const int acknack = ((uci_234->harq.harq_payload[harq_bit >> 3]) >> harq_bit) & 0x01; + for (int harq_bit = 0; harq_bit < uci_234->harq.harq_bit_len;) { NR_UE_harq_t *harq = find_harq(frame, slot, UE, RC.nrmac[mod_id]->dl_bler.harq_round_max); if (!harq) { LOG_E(NR_MAC, "UE %04x: Could not find a HARQ process at %4d.%2d!\n", UE->rnti, frame, slot); break; } DevAssert(harq->is_waiting); + const int n_ack_bits = harq->sched_pdsch.nrOfLayers > 4 ? 2 : 1; const int8_t pid = sched_ctrl->feedback_dl_harq.head; remove_front_nr_list(&sched_ctrl->feedback_dl_harq); - LOG_D(NR_MAC,"%4d.%2d bit %d pid %d ack/nack %d\n",frame, slot, harq_bit, pid, acknack); + bool success = uci_234->harq.harq_crc != 1 && harq_bit + n_ack_bits <= uci_234->harq.harq_bit_len; + for (int q = 0; q < n_ack_bits && harq_bit + q < uci_234->harq.harq_bit_len; q++) { + const int bit = harq_bit + q; + const int acknack = (uci_234->harq.harq_payload[bit >> 3] >> (bit & 7)) & 0x01; + LOG_D(NR_MAC, "%4d.%2d bit %d pid %d cw %d ack/nack %d\n", frame, slot, bit, pid, q, acknack); + success &= acknack; + } // TCI state switch occurs at the first slot that is after slot n_+ T_HARQ + 3N_sf_slot (8.10.3 of 38.133) - bool success = uci_234->harq.harq_crc != 1 && acknack; if (success && harq->start_tci_timer) { int slots = 3 * nrmac->frame_structure.numb_slots_frame / 10; nr_timer_setup(&sched_ctrl->tci_beam_switch, slots, 1); @@ -1004,6 +1060,7 @@ void handle_nr_uci_pucch_2_3_4(module_id_t mod_id, frame_t frame, slot_t slot, c harq->start_tci_timer = false; } handle_dl_harq(nrmac, UE, pid, success, nrmac->dl_bler.harq_round_max); + harq_bit += n_ack_bits; } free(uci_234->harq.harq_payload); } @@ -1353,4 +1410,3 @@ void nr_sr_reporting(gNB_MAC_INST *nrmac, frame_t SFN, slot_t slot) } } } - diff --git a/openair2/LAYER2/NR_MAC_gNB/mac_proto.h b/openair2/LAYER2/NR_MAC_gNB/mac_proto.h index 5aeace199c..dbd124ce59 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); @@ -464,6 +464,7 @@ bool prepare_initial_ul_rrc_message(gNB_MAC_INST *mac, NR_UE_info_t *UE); void send_initial_ul_rrc_message(int rnti, const uint8_t *sdu, sdu_size_t sdu_len, void *data); void finish_nr_dl_harq(NR_UE_sched_ctrl_t *sched_ctrl, int harq_pid); +void update_sched_pdsch_codeword_tbs(NR_sched_pdsch_t *sched); void abort_nr_dl_harq(NR_UE_info_t* UE, int8_t harq_pid); void nr_mac_trigger_release_timer(NR_UE_sched_ctrl_t *sched_ctrl, NR_SubcarrierSpacing_t subcarrier_spacing); diff --git a/openair2/LAYER2/NR_MAC_gNB/nr_mac_gNB.h b/openair2/LAYER2/NR_MAC_gNB/nr_mac_gNB.h index 391bfeabe8..4923309eef 100644 --- a/openair2/LAYER2/NR_MAC_gNB/nr_mac_gNB.h +++ b/openair2/LAYER2/NR_MAC_gNB/nr_mac_gNB.h @@ -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 @@ -459,6 +460,7 @@ typedef struct NR_sched_pusch { typedef struct NR_pdsch_dmrs { uint8_t dmrs_ports_id; + uint16_t dmrs_ports; uint8_t N_PRB_DMRS; uint8_t N_DMRS_SLOT; uint16_t dl_dmrs_symb_pos; @@ -484,6 +486,8 @@ typedef struct NR_sched_pdsch { uint16_t R; uint8_t Qm; uint32_t tb_size; + /// Second transport block for rank 5--8 (same MCS/R/Qm for bundled HARQ) + uint32_t tb_size2; /// DL HARQ PID to use for this UE, or -1 for "any new" int8_t dl_harq_pid; @@ -510,6 +514,7 @@ typedef struct NR_UE_harq { /* Transport block to be sent using this HARQ process */ byte_array_t transportBlock; + byte_array_t transportBlock2; uint32_t tb_size; // size of currently stored TB bool start_tci_timer; /// sched_pdsch keeps information on MCS etc used for the initial transmission @@ -546,6 +551,8 @@ struct CRI_RI_LI_PMI_CQI { uint8_t wb_cqi_2tb; uint8_t cqi_table; uint8_t csi_report_id; + uint16_t report_frame; + uint16_t report_slot; bool print_report; }; @@ -714,6 +721,9 @@ typedef struct NR_mac_dir_stats { uint32_t total_rbs_retx; uint32_t num_mac_sdu; uint32_t current_rbs; + // Most recently scheduled spatial layer count. Unlike current_rbs this is + // intentionally persistent so asynchronous E2 sampling does not miss it. + uint8_t last_layers; } NR_mac_dir_stats_t; typedef struct NR_mac_stats { @@ -965,6 +975,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 nr-gnb 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; diff --git a/openair2/LAYER2/NR_MAC_gNB/nr_radio_config.c b/openair2/LAYER2/NR_MAC_gNB/nr_radio_config.c index 9f8f9b7314..b0ace68241 100644 --- a/openair2/LAYER2/NR_MAC_gNB/nr_radio_config.c +++ b/openair2/LAYER2/NR_MAC_gNB/nr_radio_config.c @@ -32,7 +32,7 @@ #include "utils.h" #include "xer_encoder.h" -#define NR_MAX_SUPPORTED_DL_LAYERS 4 +#define NR_MAX_SUPPORTED_DL_LAYERS NR_MAX_NB_LAYERS /* Default values for measurement gap configuration */ #define DEFAULT_MGRP NR_GapConfig__mgrp_ms160 @@ -655,10 +655,18 @@ static void set_dl_maxmimolayers(NR_PDSCH_ServingCellConfig_t *pdsch_servingcell pdsch_servingcellconfig->ext1->maxMIMO_Layers = calloc(1, sizeof(*pdsch_servingcellconfig->ext1->maxMIMO_Layers)); long l = ue_supported_dl_layers(scc, uecap); - long ue_supported_layers = l > 1 ? l : 2; // min UE supported layers = 2 - if (maxMIMO_layers == -1) + // nakaolab Patch 6: when called with NULL uecap (initial config before + // capability exchange), ue_supported_dl_layers returns -1. Fall back to + // maxMIMO_layers from config directly instead of clamping to 2. + long ue_supported_layers; + if (l < 1) { + ue_supported_layers = (maxMIMO_layers > 0) ? maxMIMO_layers : NR_MAX_SUPPORTED_DL_LAYERS; + } else { + ue_supported_layers = l; + } + if (maxMIMO_layers == -1) *pdsch_servingcellconfig->ext1->maxMIMO_Layers = min(NR_MAX_SUPPORTED_DL_LAYERS, ue_supported_layers); - else + else *pdsch_servingcellconfig->ext1->maxMIMO_Layers = min(maxMIMO_layers, ue_supported_layers); } @@ -1282,7 +1290,9 @@ static void config_pucch_resset1(const NR_ServingCellConfigCommon_t *scc, pucchfmt2->interslotFrequencyHopping = NULL; pucchfmt2->additionalDMRS = NULL; pucchfmt2->maxCodeRate = calloc(1,sizeof(*pucchfmt2->maxCodeRate)); - *pucchfmt2->maxCodeRate = NR_PUCCH_MaxCodeRate_zeroDot15; + // nakaolab Patch 5: bump from zeroDot15 (19.2 bit capacity) to zeroDot25 + // (32 bit capacity) so 4-port CSI report (20 bits) fits in PUCCH. + *pucchfmt2->maxCodeRate = NR_PUCCH_MaxCodeRate_zeroDot25; pucchfmt2->nrofSlots = NULL; pucchfmt2->pi2BPSK = NULL; @@ -1691,8 +1701,14 @@ static NR_PTRS_DownlinkConfig_t *config_dlptrs(const nr_ptrs_config_t *ptrs) return ptrs_config; } -static NR_SetupRelease_PDSCH_Config_t *config_pdsch(uint64_t ssb_bitmap, int bwp_Id, bool do_TCI, nr_ptrs_config_t *ptrs) +static NR_SetupRelease_PDSCH_Config_t *config_pdsch(uint64_t ssb_bitmap, + int bwp_Id, + const nr_mac_config_t *configuration) { + const int antenna_ports = configuration->pdsch_AntennaPorts.N1 * configuration->pdsch_AntennaPorts.N2 + * configuration->pdsch_AntennaPorts.XP; + const int configured_layers = configuration->maxMIMO_layers > 0 ? configuration->maxMIMO_layers : antenna_ports; + const bool two_codewords = min(configured_layers, NR_MAX_SUPPORTED_DL_LAYERS) > 4; NR_SetupRelease_PDSCH_Config_t *setup_pdsch_Config = calloc(1,sizeof(*setup_pdsch_Config)); setup_pdsch_Config->present = NR_SetupRelease_PDSCH_Config_PR_setup; NR_PDSCH_Config_t *pdsch_Config = calloc(1, sizeof(*pdsch_Config)); @@ -1701,15 +1717,20 @@ static NR_SetupRelease_PDSCH_Config_t *config_pdsch(uint64_t ssb_bitmap, int bwp pdsch_Config->dmrs_DownlinkForPDSCH_MappingTypeA->present = NR_SetupRelease_DMRS_DownlinkConfig_PR_setup; pdsch_Config->dmrs_DownlinkForPDSCH_MappingTypeA->choice.setup = calloc(1, sizeof(*pdsch_Config->dmrs_DownlinkForPDSCH_MappingTypeA->choice.setup)); NR_DMRS_DownlinkConfig_t *dmrs_DownlinkForPDSCH_MappingTypeA = pdsch_Config->dmrs_DownlinkForPDSCH_MappingTypeA->choice.setup; - if (ptrs) { - NR_PTRS_DownlinkConfig_t *ptrs_config = config_dlptrs(ptrs); + if (configuration->ptrs && !two_codewords) { + NR_PTRS_DownlinkConfig_t *ptrs_config = config_dlptrs(configuration->ptrs); NR_SetupRelease_PTRS_DownlinkConfig_t *phaseTrackingRS = calloc(1, sizeof(*phaseTrackingRS)); phaseTrackingRS->present = NR_SetupRelease_PTRS_DownlinkConfig_PR_setup; phaseTrackingRS->choice.setup = ptrs_config; dmrs_DownlinkForPDSCH_MappingTypeA->phaseTrackingRS = phaseTrackingRS; } + if (configuration->ptrs && two_codewords) + LOG_W(NR_MAC, "PDSCH PTRS is disabled for rank 5--8 until two-codeword PTRS is supported\n"); dmrs_DownlinkForPDSCH_MappingTypeA->dmrs_Type = NULL; - dmrs_DownlinkForPDSCH_MappingTypeA->maxLength = NULL; + if (two_codewords) + asn1cCallocOne(dmrs_DownlinkForPDSCH_MappingTypeA->maxLength, NR_DMRS_DownlinkConfig__maxLength_len2); + else + dmrs_DownlinkForPDSCH_MappingTypeA->maxLength = NULL; dmrs_DownlinkForPDSCH_MappingTypeA->scramblingID0 = NULL; dmrs_DownlinkForPDSCH_MappingTypeA->scramblingID1 = NULL; dmrs_DownlinkForPDSCH_MappingTypeA->dmrs_AdditionalPosition = calloc(1, sizeof(*dmrs_DownlinkForPDSCH_MappingTypeA->dmrs_AdditionalPosition)); @@ -1718,13 +1739,16 @@ static NR_SetupRelease_PDSCH_Config_t *config_pdsch(uint64_t ssb_bitmap, int bwp pdsch_Config->dataScramblingIdentityPDSCH = NULL; pdsch_Config->resourceAllocation = NR_PDSCH_Config__resourceAllocation_resourceAllocationType1; + if (two_codewords) + asn1cCallocOne(pdsch_Config->maxNrofCodeWordsScheduledByDCI, + NR_PDSCH_Config__maxNrofCodeWordsScheduledByDCI_n2); pdsch_Config->prb_BundlingType.present = NR_PDSCH_Config__prb_BundlingType_PR_staticBundling; pdsch_Config->prb_BundlingType.choice.staticBundling = calloc(1, sizeof(*pdsch_Config->prb_BundlingType.choice.staticBundling)); pdsch_Config->prb_BundlingType.choice.staticBundling->bundleSize = calloc(1, sizeof(*pdsch_Config->prb_BundlingType.choice.staticBundling->bundleSize)); *pdsch_Config->prb_BundlingType.choice.staticBundling->bundleSize = NR_PDSCH_Config__prb_BundlingType__staticBundling__bundleSize_wideband; - if (do_TCI) { + if (configuration->do_TCI) { int n_ssb = 0; if (!pdsch_Config->tci_StatesToAddModList) pdsch_Config->tci_StatesToAddModList=calloc(1,sizeof(*pdsch_Config->tci_StatesToAddModList)); @@ -1840,7 +1864,7 @@ static NR_BWP_Downlink_t *config_downlinkBWP(const NR_ServingCellConfigCommon_t asn1cSeqAdd(&bwp->bwp_Dedicated->pdcch_Config->choice.setup->searchSpacesToAddModList->list, ss3); bwp->bwp_Dedicated->pdcch_Config->choice.setup->searchSpacesToReleaseList = NULL; - bwp->bwp_Dedicated->pdsch_Config = config_pdsch(ssb_bitmap, bwp->bwp_Id, configuration->do_TCI, configuration->ptrs); + bwp->bwp_Dedicated->pdsch_Config = config_pdsch(ssb_bitmap, bwp->bwp_Id, configuration); set_dl_mcs_table(bwp->bwp_Common->genericParameters.subcarrierSpacing, force_256qam_off ? NULL : uecap, @@ -3389,7 +3413,7 @@ static NR_BWP_DownlinkDedicated_t *configure_initial_dl_bwp(const NR_ServingCell asn1cSeqAdd(&pdcch_Config->searchSpacesToAddModList->list, ss2); bwp_Dedicated->pdcch_Config->choice.setup = pdcch_Config; - bwp_Dedicated->pdsch_Config = config_pdsch(bitmap, 0, configuration->do_TCI, configuration->ptrs); + bwp_Dedicated->pdsch_Config = config_pdsch(bitmap, 0, configuration); // we might call configuration of initial BWP for BWP switch when we already have UE capabilities set_dl_mcs_table(scc->uplinkConfigCommon->initialUplinkBWP->genericParameters.subcarrierSpacing, configuration->force_256qam_off ? NULL : uecap, @@ -4062,7 +4086,7 @@ NR_CellGroupConfig_t *get_default_secondaryCellGroup(const NR_ServingCellConfigC configDedicated->initialDownlinkBWP = calloc(1, sizeof(*configDedicated->initialDownlinkBWP)); configDedicated->initialDownlinkBWP->pdcch_Config = NULL; - configDedicated->initialDownlinkBWP->pdsch_Config = config_pdsch(bitmap, 0, configuration->do_TCI, configuration->ptrs); + configDedicated->initialDownlinkBWP->pdsch_Config = config_pdsch(bitmap, 0, configuration); configDedicated->initialDownlinkBWP->sps_Config = NULL; // calloc(1,sizeof(struct NR_SetupRelease_SPS_Config)); configDedicated->initialDownlinkBWP->radioLinkMonitoringConfig = NULL; diff --git a/radio/rfsimulator/simulator.cpp b/radio/rfsimulator/simulator.cpp index 3b2f64dbca..901c00864f 100644 --- a/radio/rfsimulator/simulator.cpp +++ b/radio/rfsimulator/simulator.cpp @@ -362,6 +362,15 @@ static buffer_t *allocCirBuf(rfsimulator_state_t *bridge, int sock) set_channeldesc_owner(ptr->channel_model, RFSIMU_MODULEID); set_channeldesc_direction(ptr->channel_model, bridge->role == SIMU_ROLE_SERVER); + // nakaolab Fix 9: ensure RNG is initialized before random_channel runs in this thread + { + static thread_local bool rng_initialized = false; + if (!rng_initialized) { + randominit(); + set_taus_seed((unsigned int)(uintptr_t)pthread_self()); + rng_initialized = true; + } + } random_channel(ptr->channel_model, false); LOG_I(HW, "Random channel %s in rfsimulator activated\n", modelname); } @@ -565,7 +574,11 @@ static void rfsimulator_readconfig(rfsimulator_state_t *rfsimulator) break; } else if (strcmp(rfsimuParam[p].strlistptr[i], "chanmod") == 0) { init_channelmod(); - load_channellist(rfsimulator->tx_num_channels, + // Channel model nb_tx must cover the peer's TX count, not just ours. + // E.g., UE has tx=1, rx=4, but gNB sends 4 TX streams. Without this fix, + // the UE-side channel model is (1,4) and only 1 TX stream is processed, + // dropping 3 of the 4 layers — RI gets capped at 1 instead of 4. + load_channellist(std::max(rfsimulator->tx_num_channels, rfsimulator->rx_num_channels), rfsimulator->rx_num_channels, rfsimulator->sample_rate, rfsimulator->rx_freq, @@ -671,7 +684,7 @@ static int rfsimu_setchanmod_cmd(char *buff, int debug, telnet_printfunc_t prnt, if (b->channel_model->model_name == NULL) continue; if (b->conn_sock >= 0 && (strcmp(b->channel_model->model_name, modelname) == 0)) { - channel_desc_t *newmodel = new_channel_desc_scm(t->tx_num_channels, + channel_desc_t *newmodel = new_channel_desc_scm(std::max(t->tx_num_channels, t->rx_num_channels), t->rx_num_channels, static_cast(channelmod), t->sample_rate, @@ -1283,6 +1296,15 @@ static void rfsimulator_read_internal(rfsimulator_state_t *t, int rx_beam_id, bool is_first_beam) { + // nakaolab Fix 9: Thread-local RNG init. + // random_channel/gaussZiggurat may run in this thread; randominit() uses + // thread_local storage, so each new RX thread needs its own init. + static thread_local bool rng_initialized = false; + if (!rng_initialized) { + randominit(); + set_taus_seed((unsigned int)(uintptr_t)pthread_self()); + rng_initialized = true; + } cf_t temp_array[nbAnt][nsamps]; bool channel_modelling = false; // Add all input nodes signal in the output buffer @@ -1306,7 +1328,23 @@ static void rfsimulator_read_internal(rfsimulator_state_t *t, } const uint64_t channel_offset = ptr->channel_model->channel_offset; const uint64_t channel_length = ptr->channel_model->channel_length; - std::vector> ant_buffers(ptr->nbAnt, std::vector(nsamps + channel_length - 1, {0, 0})); + // The UE-side channel descriptor is provisioned for the largest peer + // width it may receive (for example 8 TX when the UE has 8 RX). A + // matched experiment can still connect a narrower 4-TX gNB. Keep an + // input buffer for every descriptor TX port and leave absent peer + // ports zero; rxAddInput() iterates channel_model->nb_tx and therefore + // must not be handed the shorter peer-width pointer array. + const uint model_tx = ptr->channel_model->nb_tx; + AssertFatal(ptr->nbAnt <= model_tx, + "RFsim peer has %u TX ports but channel model supports only %u\n", + ptr->nbAnt, + model_tx); + AssertFatal(nbAnt <= ptr->channel_model->nb_rx, + "RFsim receiver has %d RX ports but channel model supports only %u\n", + nbAnt, + ptr->channel_model->nb_rx); + std::vector> ant_buffers(model_tx, + std::vector(nsamps + channel_length - 1, {0, 0})); c16_t *input[ant_buffers.size()]; for (uint aatx = 0; aatx < ant_buffers.size(); aatx++) { input[aatx] = ant_buffers[aatx].data();