diff --git a/common/utils/nr/nr_common.c b/common/utils/nr/nr_common.c index cd897f8949..b53da64f5d 100644 --- a/common/utils/nr/nr_common.c +++ b/common/utils/nr/nr_common.c @@ -13,10 +13,199 @@ #include "nr_common.h" #include #include +#include #define C_SRS_NUMBER (64) #define B_SRS_NUMBER (4) +void nr_pdsch_rank_capacity_scores(const float eigenvalues[NR_MAX_NB_LAYERS], + uint8_t max_rank, + float noise_power, + float scores[NR_MAX_NB_LAYERS]) +{ + AssertFatal(max_rank >= 1 && max_rank <= NR_MAX_NB_LAYERS, + "Invalid maximum PDSCH rank %u\n", + max_rank); + const float finite_noise = isfinite(noise_power) && noise_power > 0.0f ? noise_power : 1.0f; + for (int rank = 1; rank <= NR_MAX_NB_LAYERS; rank++) { + scores[rank - 1] = 0.0f; + if (rank > max_rank) + continue; + for (int layer = 0; layer < rank; layer++) { + const float eigenvalue = isfinite(eigenvalues[layer]) && eigenvalues[layer] > 0.0f + ? eigenvalues[layer] + : 0.0f; + scores[rank - 1] += log2f(1.0f + eigenvalue / (rank * finite_noise)); + } + } +} + +uint8_t nr_pdsch_select_rank(const float scores[NR_MAX_NB_LAYERS], + uint8_t max_rank, + float relative_margin) +{ + AssertFatal(max_rank >= 1 && max_rank <= NR_MAX_NB_LAYERS, + "Invalid maximum PDSCH rank %u\n", + max_rank); + const float margin = isfinite(relative_margin) && relative_margin > 0.0f ? relative_margin : 0.0f; + uint8_t best_rank = 1; + float best_score = isfinite(scores[0]) ? scores[0] : 0.0f; + for (uint8_t rank = 2; rank <= max_rank; rank++) { + const float candidate = isfinite(scores[rank - 1]) ? scores[rank - 1] : 0.0f; + if (candidate > best_score * (1.0f + margin)) { + best_rank = rank; + best_score = candidate; + } + } + return best_rank; +} + +uint8_t nr_type1_sp_4x1_i11_count(uint8_t layers) +{ + switch (layers) { + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + return 16; + case 7: + case 8: + return 8; + default: + return 0; + } +} + +uint8_t nr_type1_sp_4x1_i13_count(uint8_t layers) +{ + switch (layers) { + case 1: + case 5: + case 6: + case 7: + case 8: + return 1; + case 2: + return 4; + case 3: + case 4: + return 3; + default: + return 0; + } +} + +uint8_t nr_type1_sp_4x1_i2_count(uint8_t layers) +{ + if (layers == 1) + return 4; + return layers >= 2 && layers <= 8 ? 2 : 0; +} + +bool nr_type1_sp_4x1_weights(uint8_t layers, + uint8_t i11, + uint8_t i13, + uint8_t i2, + c16_t weights[NR_MAX_NB_LAYERS][NR_MAX_CSI_PORTS]) +{ + memset(weights, 0, NR_MAX_NB_LAYERS * NR_MAX_CSI_PORTS * sizeof(**weights)); + const uint8_t i11_count = nr_type1_sp_4x1_i11_count(layers); + const uint8_t i13_count = nr_type1_sp_4x1_i13_count(layers); + const uint8_t i2_count = nr_type1_sp_4x1_i2_count(layers); + if (i11_count == 0 || i11 >= i11_count || i13 >= i13_count || i2 >= i2_count) + return false; + + /* TS 38.214 Tables 5.2.2.2.1-5--12, specialized to the one-dimensional + * N1=4 panel. O1=4, so adjacent orthogonal DFT beams are four indices + * apart. Values 0..3 below encode 1, -1, phi, and -phi respectively. */ + uint8_t beam_offset[NR_MAX_NB_LAYERS] = {0}; + uint8_t polarization[NR_MAX_NB_LAYERS] = {0}; + switch (layers) { + case 1: + polarization[0] = 2; + break; + case 2: + beam_offset[1] = 4 * i13; + polarization[0] = 2; + polarization[1] = 3; + break; + case 3: + case 4: { + const uint8_t offset = 4 * (i13 + 1); + beam_offset[1] = offset; + beam_offset[3] = offset; + polarization[0] = 2; + polarization[1] = 2; + polarization[2] = 3; + polarization[3] = 3; + break; + } + case 5: { + const uint8_t offsets[] = {0, 0, 4, 4, 8}; + const uint8_t polarizations[] = {2, 3, 0, 1, 0}; + memcpy(beam_offset, offsets, sizeof(offsets)); + memcpy(polarization, polarizations, sizeof(polarizations)); + break; + } + case 6: { + const uint8_t offsets[] = {0, 0, 4, 4, 8, 8}; + const uint8_t polarizations[] = {2, 3, 2, 3, 0, 1}; + memcpy(beam_offset, offsets, sizeof(offsets)); + memcpy(polarization, polarizations, sizeof(polarizations)); + break; + } + case 7: { + const uint8_t offsets[] = {0, 0, 4, 8, 8, 12, 12}; + const uint8_t polarizations[] = {2, 3, 2, 0, 1, 0, 1}; + memcpy(beam_offset, offsets, sizeof(offsets)); + memcpy(polarization, polarizations, sizeof(polarizations)); + break; + } + case 8: { + const uint8_t offsets[] = {0, 0, 4, 4, 8, 8, 12, 12}; + const uint8_t polarizations[] = {2, 3, 2, 3, 0, 1, 0, 1}; + memcpy(beam_offset, offsets, sizeof(offsets)); + memcpy(polarization, polarizations, sizeof(polarizations)); + break; + } + } + + const float scale = 1.0f / sqrtf((float)layers); + const float phi_re = cosf((float)M_PI * i2 / 2.0f); + const float phi_im = sinf((float)M_PI * i2 / 2.0f); + for (int layer = 0; layer < layers; layer++) { + const int beam = i11 + beam_offset[layer]; + float pol_re = 1.0f; + float pol_im = 0.0f; + switch (polarization[layer]) { + case 1: + pol_re = -1.0f; + break; + case 2: + pol_re = phi_re; + pol_im = phi_im; + break; + case 3: + pol_re = -phi_re; + pol_im = -phi_im; + break; + } + + for (int element = 0; element < 4; element++) { + const float angle = 2.0f * (float)M_PI * element * beam / 16.0f; + const float beam_re = cosf(angle); + const float beam_im = sinf(angle); + weights[layer][element].r = lrintf(SHRT_MAX * scale * beam_re); + weights[layer][element].i = lrintf(SHRT_MAX * scale * beam_im); + weights[layer][element + 4].r = lrintf(SHRT_MAX * scale * (pol_re * beam_re - pol_im * beam_im)); + weights[layer][element + 4].i = lrintf(SHRT_MAX * scale * (pol_re * beam_im + pol_im * beam_re)); + } + } + return true; +} + /* TS 38.211 Table 6.4.1.4.3-1: SRS bandwidth configuration */ static const unsigned short srs_bandwidth_config[C_SRS_NUMBER][B_SRS_NUMBER][2] = { /* B_SRS = 0 B_SRS = 1 B_SRS = 2 B_SRS = 3 */ diff --git a/common/utils/nr/nr_common.h b/common/utils/nr/nr_common.h index ab93256cd6..8697d90a8c 100644 --- a/common/utils/nr/nr_common.h +++ b/common/utils/nr/nr_common.h @@ -9,6 +9,7 @@ #ifndef __COMMON_UTILS_NR_NR_COMMON__H__ #define __COMMON_UTILS_NR_NR_COMMON__H__ +#include #include #include #include "assertions.h" @@ -93,6 +94,63 @@ static inline uint8_t nr_pdsch_layers_per_codeword(uint8_t layers, uint8_t codew return layers / 2 + (codeword == 1 ? layers % 2 : 0); } +/* A two-TB PDSCH has two separate HARQ-ACK decisions even though both TBs + * share one HARQ process. ACKs for padding-only replacement codewords must + * not clear the other TB's pending retransmission. */ +static inline uint8_t nr_pdsch_pending_after_ack(uint8_t pending_mask, + uint8_t active_mask, + uint8_t ack_mask) +{ + AssertFatal(active_mask == 1 || active_mask == 3, "Invalid active codeword mask %u\n", active_mask); + const uint8_t pending = pending_mask ? pending_mask : active_mask; + return pending & ~(ack_mask & active_mask); +} + +/** Compute equal-power spatial-capacity scores for candidate PDSCH ranks. + * + * eigenvalues contains the descending eigenvalues of H^H H. The score for + * rank r is sum_i log2(1 + eigenvalue_i / (r * noise_power)): total TX power + * is held constant and divided equally among the r layers. Scores estimate + * the useful spatial multiplexing opportunity; MCS, coding, and scheduling + * overhead are intentionally left to MAC. + */ +void nr_pdsch_rank_capacity_scores(const float eigenvalues[NR_MAX_NB_LAYERS], + uint8_t max_rank, + float noise_power, + float scores[NR_MAX_NB_LAYERS]); + +/** Select the lowest rank that is materially better than all lower ranks. + * relative_margin is expressed as a fraction (0.05 means a five-percent + * required improvement), which avoids switching rank for negligible model + * gains. Returns a one-based rank. + */ +uint8_t nr_pdsch_select_rank(const float scores[NR_MAX_NB_LAYERS], + uint8_t max_rank, + float relative_margin); + +/** Number of valid i_1_1 values for the rank-1--8 Type-I single-panel + * codebooks with (N1,N2,O1,O2)=(4,1,4,1). This is the eight-port geometry + * used by the experimental rank-8 branch. Returns zero for invalid ranks. + */ +uint8_t nr_type1_sp_4x1_i11_count(uint8_t layers); + +/** Number of valid i_1_3 and i_2 values in the same codebook. */ +uint8_t nr_type1_sp_4x1_i13_count(uint8_t layers); +uint8_t nr_type1_sp_4x1_i2_count(uint8_t layers); + +/** Build one TS 38.214 Type-I single-panel rank-1--8 precoder for the + * eight-port (N1,N2,O1,O2)=(4,1,4,1), codebookMode=1 geometry. + * + * weights is indexed [layer][antenna port]. Coefficients use OAI's existing + * Q15 codebook convention (1/sqrt(layers) scaling); unused entries are zero. + * Returns false when layers, i11, i13, or i2 is outside this codebook. + */ +bool nr_type1_sp_4x1_weights(uint8_t layers, + uint8_t i11, + uint8_t i13, + uint8_t i2, + c16_t weights[NR_MAX_NB_LAYERS][NR_MAX_CSI_PORTS]); + // 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/CMakeLists.txt b/common/utils/nr/tests/CMakeLists.txt index 8695e69048..e2b403e7c7 100644 --- a/common/utils/nr/tests/CMakeLists.txt +++ b/common/utils/nr/tests/CMakeLists.txt @@ -5,3 +5,6 @@ target_link_libraries(test_nr_common PRIVATE nr_common GTest::gtest minimal_lib) add_dependencies(tests test_nr_common) add_test(NAME test_nr_common COMMAND ./test_nr_common) + +add_executable(nr_rank_selector_benchmark nr_rank_selector_benchmark.cpp) +target_link_libraries(nr_rank_selector_benchmark PRIVATE nr_common minimal_lib) diff --git a/common/utils/nr/tests/nr_rank_selector_benchmark.cpp b/common/utils/nr/tests/nr_rank_selector_benchmark.cpp new file mode 100644 index 0000000000..5c184549e6 --- /dev/null +++ b/common/utils/nr/tests/nr_rank_selector_benchmark.cpp @@ -0,0 +1,63 @@ +/* + * SPDX-License-Identifier: LicenseRef-CSSL-1.0 + */ + +#include +#include +#include + +extern "C" { +#include "nr_common.h" +} + +struct rank_scenario_t { + const char *name; + float eigenvalues[NR_MAX_NB_LAYERS]; + float noise_power; +}; + +static uint8_t legacy_rank(const float eigenvalues[NR_MAX_NB_LAYERS]) +{ + uint8_t rank = 1; + for (int layer = 1; layer < NR_MAX_NB_LAYERS; layer++) { + if (eigenvalues[layer] <= 0.0f + || 10.0f * std::log10(eigenvalues[0] / eigenvalues[layer]) >= 8.0f) + break; + rank++; + } + return rank; +} + +int main() +{ + const rank_scenario_t scenarios[] = { + {"dominant_one", {100.0f, 0.01f, 0.01f, 0.01f, 0.01f, 0.01f, 0.01f, 0.01f}, 1.0f}, + {"four_modes", {100.0f, 80.0f, 60.0f, 40.0f, 0.01f, 0.01f, 0.01f, 0.01f}, 1.0f}, + {"eight_modes", {100.0f, 95.0f, 90.0f, 85.0f, 80.0f, 75.0f, 70.0f, 65.0f}, 1.0f}, + {"balanced_low_snr", {1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}, 10.0f}, + {"legacy_false_high_rank", {1.0f, 0.16f, 0.16f, 0.16f, 0.16f, 0.16f, 0.16f, 0.16f}, 10.0f}, + {"weak_eighth", {100.0f, 90.0f, 80.0f, 70.0f, 60.0f, 50.0f, 40.0f, 5.0f}, 1.0f}, + }; + + std::puts("scenario,policy,selected_rank,selected_capacity_bps_hz"); + for (const rank_scenario_t &scenario : scenarios) { + float scores[NR_MAX_NB_LAYERS] = {}; + nr_pdsch_rank_capacity_scores(scenario.eigenvalues, + NR_MAX_NB_LAYERS, + scenario.noise_power, + scores); + const uint8_t capacity_rank = nr_pdsch_select_rank(scores, NR_MAX_NB_LAYERS, 0.05f); + const uint8_t legacy = legacy_rank(scenario.eigenvalues); + const uint8_t policies[] = {capacity_rank, legacy, 1, 4, 8}; + const char *policy_names[] = {"capacity", "legacy", "fixed1", "fixed4", "fixed8"}; + for (int policy = 0; policy < 5; policy++) { + const uint8_t rank = std::min(policies[policy], NR_MAX_NB_LAYERS); + std::printf("%s,%s,%u,%.6f\n", + scenario.name, + policy_names[policy], + rank, + scores[rank - 1]); + } + } + return 0; +} diff --git a/common/utils/nr/tests/test_nr_common.cpp b/common/utils/nr/tests/test_nr_common.cpp index e971351780..367bbd551e 100644 --- a/common/utils/nr/tests/test_nr_common.cpp +++ b/common/utils/nr/tests/test_nr_common.cpp @@ -2,6 +2,7 @@ * SPDX-License-Identifier: LicenseRef-CSSL-1.0 */ +#include #include extern "C" { #include "nr_common.h" @@ -37,6 +38,113 @@ TEST(nr_common, pdsch_codeword_layer_split) } } +TEST(nr_common, pdsch_two_codeword_ack_tracks_only_pending_transport_blocks) +{ + EXPECT_EQ(nr_pdsch_pending_after_ack(0, 3, 0), 3); // both NACK or DTX + EXPECT_EQ(nr_pdsch_pending_after_ack(0, 3, 1), 2); // CW0 ACK, CW1 NACK + EXPECT_EQ(nr_pdsch_pending_after_ack(0, 3, 2), 1); // CW0 NACK, CW1 ACK + EXPECT_EQ(nr_pdsch_pending_after_ack(0, 3, 3), 0); // both ACK + EXPECT_EQ(nr_pdsch_pending_after_ack(2, 3, 1), 2); // padding CW0 ACK is irrelevant + EXPECT_EQ(nr_pdsch_pending_after_ack(2, 3, 2), 0); // pending CW1 ACK finishes + EXPECT_EQ(nr_pdsch_pending_after_ack(1, 3, 2), 1); // padding CW1 ACK is irrelevant + EXPECT_EQ(nr_pdsch_pending_after_ack(1, 3, 1), 0); // pending CW0 ACK finishes + EXPECT_EQ(nr_pdsch_pending_after_ack(0, 1, 1), 0); // one-codeword legacy HARQ +} + +TEST(nr_common, pdsch_rank_capacity_prefers_independent_modes) +{ + float eigenvalues[NR_MAX_NB_LAYERS]; + for (float &eigenvalue : eigenvalues) + eigenvalue = 100.0f; + float scores[NR_MAX_NB_LAYERS] = {}; + nr_pdsch_rank_capacity_scores(eigenvalues, NR_MAX_NB_LAYERS, 1.0f, scores); + + for (int rank = 1; rank < NR_MAX_NB_LAYERS; rank++) + EXPECT_GT(scores[rank], scores[rank - 1]); + EXPECT_EQ(nr_pdsch_select_rank(scores, NR_MAX_NB_LAYERS, 0.05f), 8); +} + +TEST(nr_common, pdsch_rank_capacity_rejects_weak_modes) +{ + float eigenvalues[NR_MAX_NB_LAYERS] = {100.0f, 0.01f, 0.01f, 0.01f, 0.01f, 0.01f, 0.01f, 0.01f}; + float scores[NR_MAX_NB_LAYERS] = {}; + nr_pdsch_rank_capacity_scores(eigenvalues, NR_MAX_NB_LAYERS, 1.0f, scores); + + EXPECT_EQ(nr_pdsch_select_rank(scores, NR_MAX_NB_LAYERS, 0.05f), 1); +} + +TEST(nr_common, pdsch_rank_capacity_respects_rank_and_gain_margin) +{ + const float scores[NR_MAX_NB_LAYERS] = {10.0f, 10.4f, 10.6f, 20.0f, 30.0f, 40.0f, 50.0f, 60.0f}; + EXPECT_EQ(nr_pdsch_select_rank(scores, 3, 0.05f), 3); + EXPECT_EQ(nr_pdsch_select_rank(scores, 2, 0.05f), 1); +} + +TEST(nr_common, type1_sp_4x1_parameter_ranges) +{ + constexpr uint8_t expected_i11[] = {16, 16, 16, 16, 16, 16, 8, 8}; + constexpr uint8_t expected_i13[] = {1, 4, 3, 3, 1, 1, 1, 1}; + constexpr uint8_t expected_i2[] = {4, 2, 2, 2, 2, 2, 2, 2}; + for (uint8_t layers = 1; layers <= NR_MAX_NB_LAYERS; layers++) { + EXPECT_EQ(nr_type1_sp_4x1_i11_count(layers), expected_i11[layers - 1]); + EXPECT_EQ(nr_type1_sp_4x1_i13_count(layers), expected_i13[layers - 1]); + EXPECT_EQ(nr_type1_sp_4x1_i2_count(layers), expected_i2[layers - 1]); + } + + c16_t weights[NR_MAX_NB_LAYERS][NR_MAX_CSI_PORTS]; + EXPECT_FALSE(nr_type1_sp_4x1_weights(0, 0, 0, 0, weights)); + EXPECT_FALSE(nr_type1_sp_4x1_weights(8, 8, 0, 0, weights)); + EXPECT_FALSE(nr_type1_sp_4x1_weights(8, 0, 1, 0, weights)); + EXPECT_FALSE(nr_type1_sp_4x1_weights(8, 0, 0, 2, weights)); +} + +TEST(nr_common, type1_sp_4x1_precoders_are_orthogonal) +{ + for (uint8_t layers = 1; layers <= NR_MAX_NB_LAYERS; layers++) { + const uint8_t i11_count = nr_type1_sp_4x1_i11_count(layers); + for (uint8_t i11 = 0; i11 < i11_count; i11++) { + for (uint8_t i13 = 0; i13 < nr_type1_sp_4x1_i13_count(layers); i13++) { + for (uint8_t i2 = 0; i2 < nr_type1_sp_4x1_i2_count(layers); i2++) { + c16_t weights[NR_MAX_NB_LAYERS][NR_MAX_CSI_PORTS]; + ASSERT_TRUE(nr_type1_sp_4x1_weights(layers, i11, i13, i2, weights)); + const int64_t expected_norm = 8LL * SHRT_MAX * SHRT_MAX / layers; + const int64_t tolerance = expected_norm / 1000; + for (int a = 0; a < layers; a++) { + int64_t diagonal = 0; + for (int port = 0; port < 8; port++) + diagonal += (int64_t)weights[a][port].r * weights[a][port].r + + (int64_t)weights[a][port].i * weights[a][port].i; + EXPECT_NEAR((double)diagonal, (double)expected_norm, (double)tolerance); + for (int b = a + 1; b < layers; b++) { + int64_t inner_re = 0; + int64_t inner_im = 0; + for (int port = 0; port < 8; port++) { + inner_re += (int64_t)weights[a][port].r * weights[b][port].r + + (int64_t)weights[a][port].i * weights[b][port].i; + inner_im += (int64_t)weights[a][port].r * weights[b][port].i + - (int64_t)weights[a][port].i * weights[b][port].r; + } + EXPECT_NEAR((double)inner_re, 0.0, (double)tolerance); + EXPECT_NEAR((double)inner_im, 0.0, (double)tolerance); + } + } + } + } + } + } +} + +TEST(nr_common, type1_sp_4x1_i2_rotates_cross_polarization) +{ + c16_t i2_zero[NR_MAX_NB_LAYERS][NR_MAX_CSI_PORTS]; + c16_t i2_one[NR_MAX_NB_LAYERS][NR_MAX_CSI_PORTS]; + ASSERT_TRUE(nr_type1_sp_4x1_weights(8, 0, 0, 0, i2_zero)); + ASSERT_TRUE(nr_type1_sp_4x1_weights(8, 0, 0, 1, i2_one)); + EXPECT_EQ(i2_zero[0][4].i, 0); + EXPECT_EQ(i2_one[0][4].r, 0); + EXPECT_EQ(i2_zero[0][4].r, i2_one[0][4].i); +} + int main(int argc, char **argv) { diff --git a/docker/Dockerfile.nakaolab.test b/docker/Dockerfile.nakaolab.test new file mode 100644 index 0000000000..f8a5389688 --- /dev/null +++ b/docker/Dockerfile.nakaolab.test @@ -0,0 +1,67 @@ +FROM ran-base:latest AS ran-build +ARG BUILD_OPTION + +RUN rm -Rf /oai-ran +WORKDIR /oai-ran +COPY . . + +# build all targets so final targets can be created afterwards +RUN /bin/sh oaienv && \ + cd cmake_targets && \ + ./build_oai --ninja \ + --gNB --RU --nrUE -w SIMU -w USRP --build-lib telnetsrv \ + --build-e2 --cmake-opt -DKPM_VERSION=KPM_V3_00 --cmake-opt -DE2AP_VERSION=E2AP_V3 \ + --cmake-opt -DCMAKE_C_FLAGS="-Werror" --cmake-opt -DCMAKE_CXX_FLAGS="-Werror" $BUILD_OPTION && \ + echo "---- ldd on executables ----" && \ + ldd ran_build/build/*softmodem* ran_build/build/nr-cuup ran_build/build/oairu && \ + echo "---- ldd on shared libraries ----" && \ + ldd ran_build/build/*.so + +RUN set -eux; \ + apt-get update; \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + ca-certificates curl wget iproute2 iputils-ping dnsutils jq \ + iperf iperf3 ffmpeg gdb\ + nghttp2-client || true; \ + if ! command -v h2load >/dev/null 2>&1; then \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends nghttp2 || true; \ + fi; \ + wget -O /usr/local/bin/sipp https://github.com/SIPp/sipp/releases/download/v3.7.7/sipp && chmod +x /usr/local/bin/sipp; \ + rm -rf /var/lib/apt/lists/* + +# FlexRIC libraries (built by --build-e2) - only copy service model libraries (*_sm.so). +# The source tree can carry a pre-existing FlexRIC build directory whose object-file +# timestamps are newer than freshly copied headers. Force the MAC service model and +# xApp to rebuild so mac_ind_msg_t has one ABI in the gNB, plugin, and xApp. Install +# plugins only after that rebuild. +RUN mkdir -p /usr/local/etc/flexric /usr/local/lib/flexric && \ + cp /oai-ran/openair2/E2AP/flexric/flexric.conf /usr/local/etc/flexric/ 2>/dev/null || true && \ + cd /oai-ran/openair2/E2AP/flexric/build && \ + sed -i 's|/home/[^/]*/openairinterface5g|/oai-ran|g' CMakeCache.txt && \ + find . -name 'Makefile' -o -name '*.cmake' | xargs sed -i 's|/home/[^/]*/openairinterface5g|/oai-ran|g' 2>/dev/null || true && \ + find src/sm/mac_sm/CMakeFiles/mac_sm.dir src/sm/mac_sm/CMakeFiles/mac_sm_static.dir \ + -type f \( -name '*.o' -o -name '*.o.d' \) -delete && \ + rm -f src/sm/mac_sm/libmac_sm.so src/sm/mac_sm/libmac_sm_static.a \ + examples/xApp/c/monitor/CMakeFiles/xapp_nakaolab.dir/xapp_nakaolab.c.o \ + examples/xApp/c/monitor/CMakeFiles/xapp_nakaolab.dir/xapp_nakaolab.c.o.d && \ + cmake --build . --target mac_sm mac_sm_static xapp_nakaolab -j2 && \ + find /oai-ran/openair2/E2AP/flexric/build -name "*_sm.so" -exec cp {} /usr/local/lib/flexric/ \; && \ + echo "/usr/local/lib/flexric" > /etc/ld.so.conf.d/flexric.conf && ldconfig && \ + ls -la /usr/local/lib/flexric/ + +# Install netcat for antenna control testing + paramiko for M-Plane NETCONF server +RUN apt-get update && apt-get install -y --no-install-recommends \ + netcat-openbsd python3-pip \ + && pip3 install --break-system-packages paramiko \ + && rm -rf /var/lib/apt/lists/* + +# O-RU M-Plane NETCONF server (o-ran-uplane-conf antenna mask control) +# Listens on port 831, translates NETCONF edit-config to internal antenna control +COPY mplane/mplane_server.py /opt/mplane/mplane_server.py +COPY mplane/wg4-capability.json /opt/mplane/wg4-capability.json +COPY mplane/gnb-mplane-entrypoint.sh /opt/mplane/gnb-mplane-entrypoint.sh +RUN chmod +x /opt/mplane/gnb-mplane-entrypoint.sh + +EXPOSE 831 + +ENTRYPOINT ["/bin/bash"] diff --git a/executables/nr-uesoftmodem.c b/executables/nr-uesoftmodem.c index 7997f72ef5..8dfda8ba53 100644 --- a/executables/nr-uesoftmodem.c +++ b/executables/nr-uesoftmodem.c @@ -246,6 +246,12 @@ int main(int argc, char **argv) softmodem_verify_mode(get_softmodem_params()); + const char *cpu_meas_env = getenv("OAI_CPU_MEAS"); + if (cpu_meas_env != NULL && strcmp(cpu_meas_env, "1") == 0) { + cpumeas(CPUMEAS_ENABLE); + LOG_I(UTIL, "UE PHY CPU measurements enabled by OAI_CPU_MEAS=1\n"); + } + #if T_TRACER T_Config_Init(); #endif @@ -490,4 +496,3 @@ int main(int argc, char **argv) printf("Bye.\n"); return 0; } - diff --git a/mplane/__pycache__/mplane_server.cpython-312.pyc b/mplane/__pycache__/mplane_server.cpython-312.pyc new file mode 100644 index 0000000000..21368c7f9a Binary files /dev/null and b/mplane/__pycache__/mplane_server.cpython-312.pyc differ diff --git a/mplane/gnb-mplane-entrypoint.sh b/mplane/gnb-mplane-entrypoint.sh new file mode 100644 index 0000000000..b17fdb1dab --- /dev/null +++ b/mplane/gnb-mplane-entrypoint.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# gNB entrypoint with M-Plane NETCONF server +# +# Starts the M-Plane server alongside the gNB process. +# The M-Plane server acts as an O-RU NETCONF endpoint, +# translating o-ran-uplane-conf edit-config to internal antenna control. +# +# Usage: gnb-mplane-entrypoint.sh +# Example: gnb-mplane-entrypoint.sh /oai-ran/.../nr-softmodem -O /opt/.../gnb.conf --rfsim + +set -e + +echo "[gnb-mplane] Starting M-Plane NETCONF server (port ${MPLANE_PORT:-831})..." +python3 /opt/mplane/mplane_server.py & +MPLANE_PID=$! + +# Give the M-Plane server a moment to start +sleep 1 + +if ! kill -0 $MPLANE_PID 2>/dev/null; then + echo "[gnb-mplane] WARNING: M-Plane server failed to start, continuing without it" +fi + +# Run the gNB command (passed as arguments) +echo "[gnb-mplane] Starting gNB: $@" +exec "$@" diff --git a/mplane/mplane_server.py b/mplane/mplane_server.py new file mode 100644 index 0000000000..9520056922 --- /dev/null +++ b/mplane/mplane_server.py @@ -0,0 +1,622 @@ +#!/usr/bin/env python3 + +""" +O-RU M-Plane NETCONF Server for gNB Antenna Control + +Lightweight NETCONF/SSH server implementing the O-RAN WG4 M-Plane interface +for antenna mask configuration (o-ran-uplane-conf.yang). + +The gNB acts as an O-RU for antenna control purposes: + - Advertises mplane-supported-trx-control-masks (capability report) + - Accepts tx-array-antenna-mask-config via NETCONF edit-config + - Translates received mask to internal antenna control (localhost:9999) + - Sends mplane-trx-control-ant-mask-update notification (logged) + +Protocol: + SSH transport (paramiko) -> NETCONF base:1.0 framing -> XML RPC processing + +Architecture: + o1-adapter (O-DU) --NETCONF:831--> mplane_server.py (O-RU) --localhost:9999--> nr_antenna_control.c +""" + +import base64 +import json +import logging +import os +import re +import signal +import socket +import sys +import threading +import time +import xml.etree.ElementTree as ET + +import paramiko + +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- + +MPLANE_PORT = 831 +MPLANE_USER = MPLANE_PASS = "netconf" +GNB_CTRL_HOST = "127.0.0.1" +GNB_CTRL_PORT = 9999 +WG4_CAPABILITY_FILE = "/opt/mplane/wg4-capability.json" +SSH_HOST_KEY = "/opt/mplane/ssh_host_rsa_key" + +NS_NC = "urn:ietf:params:xml:ns:netconf:base:1.0" +NS_UPLANE = "urn:o-ran:uplane-conf:1.0" +NETCONF_END = "]]>]]>" # https://www.rfc-editor.org/rfc/rfc4742 NETCONF base:1.0 message delimiter + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(levelname)s %(name)s: %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + stream=sys.stdout, +) +log = logging.getLogger("mplane-server") + +# WG4 Capability (O-RU supported masks) +def b64_to_mask(b64_str): + return int.from_bytes(base64.b64decode(b64_str), byteorder="big") + +def mask_to_b64(mask_int): + byte_len = max(1, (mask_int.bit_length() + 7) // 8) + return base64.b64encode(mask_int.to_bytes(byte_len, byteorder="big")).decode() + + +class Capability: + """O-RU capability report: tx-arrays with supported masks.""" + + def __init__(self): + self.tx_arrays = {} # name -> {"masks": [{index, b64, int}], ...} + self.current_config = {} # array-name -> {"index": i, "mask_int": m} + + def load(self, path): + if not (path and os.path.exists(path)): + # Fallback: generate for 4 antennas + masks = [] + for n in range(4, 0, -1): + m = (1 << n) - 1 + masks.append({"index": 4 - n, "b64": mask_to_b64(m), "int": m}) + self.tx_arrays["tx-array-0"] = {"masks": masks, "columns": 4, "layers": 4} + self.current_config["tx-array-0"] = {"index": 0, "mask_int": 0x0F} + log.info("Generated fallback capability (4 antennas)") + return + + with open(path) as f: + data = json.load(f) # returns a dict + for arr in data.get("tx-arrays", []): # blank list for fallback + name = arr["name"] + cap = arr.get("mplane-trx-control-txarr-capability-info", {}) + masks = [] + for m in cap.get("mplane-supported-trx-control-masks", []): + masks.append({ + "index": m["index"], + "b64": m["antenna-mask"], + "int": b64_to_mask(m["antenna-mask"]), + }) + self.tx_arrays[name] = { + "masks": masks, + "columns": arr.get("number-of-columns", 4), + "layers": arr.get("number-of-array-layers", 4), + } + # Default: all active (index 0) + if masks: + self.current_config[name] = { + "index": masks[0]["index"], + "mask_int": masks[0]["int"], + } + log.info("Loaded capability: %d tx-arrays from %s", len(self.tx_arrays), path) + + def resolve_index(self, array_name, index): + arr = self.tx_arrays.get(array_name) + if not arr: + return None, f"unknown tx-array '{array_name}'" + for m in arr["masks"]: + if m["index"] == index: + return m, "" + return None, f"index {index} not in supported masks" + + def resolve_mask(self, array_name, mask_int): + arr = self.tx_arrays.get(array_name) + if not arr: + return None, f"unknown tx-array '{array_name}'" + for m in arr["masks"]: + if m["int"] == mask_int: + return m, "" + return None, f"mask 0x{mask_int:02X} not in supported masks" + + def get_capability_xml(self): + """Generate o-ran-uplane-conf operational data XML for response.""" + parts = [f''] + for name, arr in self.tx_arrays.items(): + parts.append(f"{name}") + parts.append(f"{arr['columns']}") + parts.append(f"{arr['layers']}") + parts.append("") + for m in arr["masks"]: + parts.append("") + parts.append(f"{m['index']}") + parts.append(f"{m['b64']}") + parts.append("") + parts.append("") + parts.append("") + # Current config + for aname, cfg in self.current_config.items(): + parts.append("") + parts.append(f"{aname}") + parts.append(f"{cfg['index']}") + parts.append("") + parts.append("") + return "".join(parts) + + +# --------------------------------------------------------------------------- +# gNB internal antenna control client (localhost:9999) +# --------------------------------------------------------------------------- + +class GnbCtrlClient: + """Connects to the gNB internal test socket to apply antenna masks.""" + + def __init__(self, host, port): + self.host = host + self.port = port + self.sock = None + + def connect(self): + for attempt in range(30): + try: + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock.settimeout(5.0) + self.sock.connect((self.host, self.port)) + self._recv() # consume welcome + log.info("Connected to gNB control at %s:%d", self.host, self.port) + return True + except (socket.error, ConnectionRefusedError) as e: + log.debug("gNB ctrl connect attempt %d/30: %s", attempt + 1, e) + if self.sock: + self.sock.close() + self.sock = None + time.sleep(2) + log.warning("Cannot connect to gNB control at %s:%d", self.host, self.port) + return False + + def send_command(self, cmd): + if not self.sock: + if not self.connect(): + return "ERROR: not connected" + try: + self.sock.sendall((cmd + "\n").encode()) + time.sleep(0.1) + return self._recv() + except (socket.error, BrokenPipeError): + log.warning("gNB ctrl connection lost, reconnecting...") + self.sock = None + if not self.connect(): + return "ERROR: reconnect failed" + self.sock.sendall((cmd + "\n").encode()) + time.sleep(0.1) + return self._recv() + + def _recv(self): + self.sock.settimeout(0.5) + data = b"" + try: + while True: + chunk = self.sock.recv(4096) + if not chunk: + break + data += chunk + except socket.timeout: + pass + return data.decode("utf-8", errors="ignore") + + def close(self): + if self.sock: + try: + self.sock.sendall(b"QUIT\n") + except Exception: + pass + self.sock.close() + self.sock = None + + +# --------------------------------------------------------------------------- +# NETCONF Session Handler +# --------------------------------------------------------------------------- + +class NetconfSession: + """Handle a single NETCONF session on an SSH channel.""" + + def __init__(self, channel, capability, gnb_ctrl): + self.channel = channel + self.cap = capability + self.gnb = gnb_ctrl + self.session_id = 1 + self.running = True + + def run(self): + try: + self._send_hello() + hello_msg = self._recv_message() + if hello_msg is None: + log.warning("Client did not send hello") + return + log.info("NETCONF hello exchange complete") + + while self.running: + msg = self._recv_message() + if msg is None: + break + self._process_rpc(msg) + except Exception as e: + log.exception("Session error: %s", e) + finally: + try: + self.channel.close() + except Exception: + pass + log.info("NETCONF session closed") + + def _send_hello(self): + hello = ( + '' + f'' + "" + "urn:ietf:params:netconf:base:1.0" + "urn:ietf:params:netconf:capability:writable-running:1.0" + "" + f"{self.session_id}" + "" + ) + self._send_message(hello) + + def _send_message(self, xml_str): + data = (xml_str + NETCONF_END).encode("utf-8") + self.channel.sendall(data) + + def _recv_message(self): + buf = b"" + while self.running: + try: + self.channel.settimeout(300) + chunk = self.channel.recv(65536) + if not chunk: + return None + buf += chunk + text = buf.decode("utf-8", errors="ignore") + if NETCONF_END in text: + return text.split(NETCONF_END)[0].strip() + except socket.timeout: + continue + except Exception: + return None + return None + + def _process_rpc(self, xml_str): + # Strip XML declaration if present + xml_str = re.sub(r"<\?xml[^?]*\?>", "", xml_str).strip() + try: + root = ET.fromstring(xml_str) + except ET.ParseError as e: + log.warning("Malformed RPC: %s", e) + self._send_error("0", "malformed-message", "rpc") + return + + msg_id = root.get("message-id", "0") + + # Find the operation element + for child in root: + local_tag = child.tag.split("}")[-1] if "}" in child.tag else child.tag + + if local_tag == "edit-config": + self._handle_edit_config(msg_id, child) + elif local_tag == "get": + self._handle_get(msg_id, child) + elif local_tag == "get-config": + self._handle_get_config(msg_id, child) + elif local_tag == "close-session": + self._send_ok(msg_id) + self.running = False + elif local_tag == "lock" or local_tag == "unlock": + self._send_ok(msg_id) + else: + self._send_error(msg_id, "operation-not-supported", "protocol") + return # Only process first operation + + def _handle_edit_config(self, msg_id, edit_elem): + """Handle edit-config for tx-array-antenna-mask-config.""" + # Find element + config_elem = None + for child in edit_elem: + local = child.tag.split("}")[-1] if "}" in child.tag else child.tag + if local == "config": + config_elem = child + break + + if config_elem is None: + self._send_error(msg_id, "missing-element", "protocol", + "Missing element") + return + + # Find tx-array-antenna-mask-config under user-plane-configuration + mask_config = None + for elem in config_elem.iter(): + local = elem.tag.split("}")[-1] if "}" in elem.tag else elem.tag + if local == "tx-array-antenna-mask-config": + mask_config = elem + break + + if mask_config is None: + # No antenna mask config in this edit - accept silently + self._send_ok(msg_id) + return + + # Extract array-name, antenna-bitmask-index or antenna-bitmask + array_name = None + bitmask_index = None + bitmask_b64 = None + + for child in mask_config: + local = child.tag.split("}")[-1] if "}" in child.tag else child.tag + if local == "array-name": + array_name = child.text.strip() if child.text else None + elif local == "antenna-bitmask-index": + bitmask_index = int(child.text.strip()) if child.text else None + elif local == "antenna-bitmask": + bitmask_b64 = child.text.strip() if child.text else None + + if not array_name: + self._send_error(msg_id, "missing-element", "application", + "Missing array-name in tx-array-antenna-mask-config") + return + + # Resolve the mask + resolved = None + if bitmask_index is not None: + resolved, err = self.cap.resolve_index(array_name, bitmask_index) + if not resolved: + self._send_error(msg_id, "invalid-value", "application", err) + return + log.info("M-Plane edit-config: array=%s, antenna-bitmask-index=%d", + array_name, bitmask_index) + elif bitmask_b64 is not None: + mask_int = b64_to_mask(bitmask_b64) + resolved, err = self.cap.resolve_mask(array_name, mask_int) + if not resolved: + self._send_error(msg_id, "invalid-value", "application", err) + return + log.info("M-Plane edit-config: array=%s, antenna-bitmask=%s (0x%02X)", + array_name, bitmask_b64, mask_int) + else: + self._send_error(msg_id, "missing-element", "application", + "Need antenna-bitmask or antenna-bitmask-index") + return + + # Apply to gNB via internal control socket and verify it succeeded. + # The gNB returns "OK: TX mask 0xAA -> 0xBB" on success; the socket + # wrapper returns "ERROR: ..." on transport failure and the gNB itself + # returns "ERROR: ..." for invalid input. Returning when the + # underlying control failed would let the M-Plane state and the gNB + # state diverge. + mask_int = resolved["int"] + resp = self.gnb.send_command(f"SET TX MASK 0x{mask_int:02X}") + resp_clean = (resp or "").strip() + + if not resp_clean.upper().startswith("OK"): + log.warning("gNB rejected mask 0x%02X: %r", mask_int, resp_clean) + self._send_error( + msg_id, + "operation-failed", + "application", + f"gNB control did not confirm mask change: {resp_clean or 'no response'}", + ) + return + + log.info("Applied mask 0x%02X to gNB: %s", mask_int, resp_clean) + + # Update current config only after the gNB confirmed. + self.cap.current_config[array_name] = { + "index": resolved["index"], + "mask_int": mask_int, + } + # Note: the production O1 path used by this paper runs through + # docker/o1-adapter/o1_antenna_bridge.py; this legacy gNB-side + # M-Plane server keeps the same OK-required check for parity. + + # Log WG4 mplane-trx-control-ant-mask-update notification + log.info("WG4 mplane-trx-control-ant-mask-update: " + "array-name='%s', array-type=TX-ARRAY, " + "updated-antenna-mask-index=%d, updated-antenna-mask=%s", + array_name, resolved["index"], resolved["b64"]) + + self._send_ok(msg_id) + + def _handle_get(self, msg_id, get_elem): + """Handle - return operational data (capability + current config).""" + data_xml = self.cap.get_capability_xml() + reply = ( + f'' + f"{data_xml}" + "" + ) + self._send_message(reply) + + def _handle_get_config(self, msg_id, get_elem): + """Handle - return running config (current mask config).""" + parts = [f''] + for aname, cfg in self.cap.current_config.items(): + parts.append("") + parts.append(f"{aname}") + parts.append(f"{cfg['index']}") + parts.append("") + parts.append("") + reply = ( + f'' + f'{"".join(parts)}' + "" + ) + self._send_message(reply) + + def _send_ok(self, msg_id): + reply = ( + f'' + "" + "" + ) + self._send_message(reply) + + def _send_error(self, msg_id, error_tag, error_type, error_message=""): + msg_part = "" + if error_message: + msg_part = f"{error_message}" + reply = ( + f'' + "" + f"{error_type}" + f"{error_tag}" + "error" + f"{msg_part}" + "" + "" + ) + self._send_message(reply) + + +# --------------------------------------------------------------------------- +# SSH Server Interface +# --------------------------------------------------------------------------- + +class MplaneSSHServer(paramiko.ServerInterface): + def __init__(self): + self.subsystem_event = threading.Event() + + def check_channel_request(self, kind, chanid): + if kind == "session": + return paramiko.OPEN_SUCCEEDED + return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED + + def get_allowed_auths(self, username): + return "password" + + def check_auth_password(self, username, password): + if username == MPLANE_USER and password == MPLANE_PASS: + return paramiko.AUTH_SUCCESSFUL + return paramiko.AUTH_FAILED + + def check_channel_subsystem_request(self, channel, name): + if name == "netconf": + self.subsystem_event.set() + return True + return False + + +# --------------------------------------------------------------------------- +# Main Server +# --------------------------------------------------------------------------- + +def handle_connection(client_sock, addr, host_key, capability, gnb_ctrl): + """Handle a single SSH/NETCONF client connection.""" + log.info("SSH connection from %s:%d", addr[0], addr[1]) + transport = None + try: + transport = paramiko.Transport(client_sock) + transport.add_server_key(host_key) + server = MplaneSSHServer() + transport.start_server(server=server) + + channel = transport.accept(30) + if channel is None: + log.warning("No channel opened by client") + return + + # Wait for NETCONF subsystem request + if not server.subsystem_event.wait(10): + log.warning("Client did not request netconf subsystem") + channel.close() + return + + session = NetconfSession(channel, capability, gnb_ctrl) + session.run() + + except paramiko.SSHException as e: + log.warning("SSH error from %s: %s", addr[0], e) + except Exception as e: + log.exception("Connection error from %s: %s", addr[0], e) + finally: + if transport: + try: + transport.close() + except Exception: + pass + + +def main(): + log.info("O-RU M-Plane NETCONF server starting") + log.info(" Port: %d", MPLANE_PORT) + log.info(" gNB control: %s:%d (internal)", GNB_CTRL_HOST, GNB_CTRL_PORT) + + # Load/generate SSH host key + if os.path.exists(SSH_HOST_KEY): + host_key = paramiko.RSAKey.from_private_key_file(SSH_HOST_KEY) + log.info("Loaded SSH host key from %s", SSH_HOST_KEY) + else: + host_key = paramiko.RSAKey.generate(2048) + os.makedirs(os.path.dirname(SSH_HOST_KEY), exist_ok=True) + host_key.write_private_key_file(SSH_HOST_KEY) + log.info("Generated SSH host key at %s", SSH_HOST_KEY) + + # Load WG4 capability + capability = Capability() + capability.load(WG4_CAPABILITY_FILE) + for name, arr in capability.tx_arrays.items(): + log.info(" tx-array '%s': %d supported masks", name, len(arr["masks"])) + for m in arr["masks"]: + log.info(" index=%d: %s (0x%02X, %d active)", + m["index"], m["b64"], m["int"], bin(m["int"]).count("1")) + + # Connect to gNB internal control + gnb_ctrl = GnbCtrlClient(GNB_CTRL_HOST, GNB_CTRL_PORT) + gnb_ctrl.connect() + + # Create server socket + server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server_sock.bind(("0.0.0.0", MPLANE_PORT)) + server_sock.listen(5) + server_sock.settimeout(1.0) + + shutdown = threading.Event() + + def handle_signal(signum, frame): + log.info("Shutdown signal received") + shutdown.set() + + signal.signal(signal.SIGTERM, handle_signal) + signal.signal(signal.SIGINT, handle_signal) + + log.info("M-Plane NETCONF server ready on port %d", MPLANE_PORT) + + while not shutdown.is_set(): + try: + client_sock, addr = server_sock.accept() + t = threading.Thread( + target=handle_connection, + args=(client_sock, addr, host_key, capability, gnb_ctrl), + daemon=True, + ) + t.start() + except socket.timeout: + continue + except Exception as e: + if not shutdown.is_set(): + log.exception("Accept error: %s", e) + + log.info("Shutting down...") + server_sock.close() + gnb_ctrl.close() + + +if __name__ == "__main__": + main() diff --git a/mplane/wg4-capability.json b/mplane/wg4-capability.json new file mode 100644 index 0000000000..551ba2f4de --- /dev/null +++ b/mplane/wg4-capability.json @@ -0,0 +1,17 @@ +{ + "tx-arrays": [ + { + "name": "tx-array-0", + "number-of-columns": 4, + "number-of-array-layers": 4, + "mplane-trx-control-txarr-capability-info": { + "mplane-supported-trx-control-masks": [ + {"index": 0, "antenna-mask": "Dw=="}, + {"index": 1, "antenna-mask": "Bw=="}, + {"index": 2, "antenna-mask": "Aw=="}, + {"index": 3, "antenna-mask": "AQ=="} + ] + } + } + ] +} diff --git a/nfapi/open-nFAPI/nfapi/public_inc/fapi_nr_ue_interface.h b/nfapi/open-nFAPI/nfapi/public_inc/fapi_nr_ue_interface.h index b66fdc9b41..f1c7e60efa 100644 --- a/nfapi/open-nFAPI/nfapi/public_inc/fapi_nr_ue_interface.h +++ b/nfapi/open-nFAPI/nfapi/public_inc/fapi_nr_ue_interface.h @@ -463,6 +463,11 @@ typedef struct { uint8_t power_control_offset; // Ratio of PDSCH EPRE to NZP CSI-RSEPRE [3GPP TS 38.214, sec 5.2.2.3.1], Value: 0->23 representing -8 to 15 dB in 1dB steps; 255: L1 is configured with ProfileSSS uint8_t power_control_offset_ss; // Ratio of NZP CSI-RS EPRE to SSB/PBCH block EPRE [3GPP TS 38.214, sec 5.2.2.3.1], Values: 0: -3dB; 1: 0dB; 2: 3dB; 3: 6dB; 255: L1 is configured with ProfileSSS uint8_t measurement_bitmap; // bit 0 RSRP, bit 1 RI, bit 2 LI, bit 3 PMI, bit 4 CQI, bit 5 i1 + // Internal UE MAC-to-PHY context for Type-I single-panel PMI search. Zero + // means that no associated codebook report was found for this CSI-RS. + uint8_t codebook_n1; + uint8_t codebook_n2; + uint8_t codebook_mode; } fapi_nr_dl_config_csirs_pdu_rel15_t; typedef enum{vrb_to_prb_mapping_non_interleaved = 0, vrb_to_prb_mapping_interleaved = 1} vrb_to_prb_mapping_t; @@ -748,4 +753,3 @@ typedef struct { } fapi_nr_config_request_t; #endif - diff --git a/openair1/PHY/CODING/nrPolar_tools/nr_polar_procedures.c b/openair1/PHY/CODING/nrPolar_tools/nr_polar_procedures.c index 4db0e876b9..c5fa15605e 100644 --- a/openair1/PHY/CODING/nrPolar_tools/nr_polar_procedures.c +++ b/openair1/PHY/CODING/nrPolar_tools/nr_polar_procedures.c @@ -246,10 +246,31 @@ void nr_polar_info_bit_pattern(uint8_t *ibp, } if (n_PC > 0) { - AssertFatal(n_pc_wm == 0, "Q_PC_N computation for n_pc_wm = %i is not implemented yet!\n", n_pc_wm); + AssertFatal(n_pc_wm <= 1, "n_pc_wm = %i is not valid (only 0 and 1 allowed)\n", n_pc_wm); + + // As per 5.3.1.2 of 38.212, denote the set of bit indices for parity check bits as Q_PC_N + // A number of (n_PC - n_pc_wm) parity check bits are placed in the (n_PC - n_pc_wm) least reliable bit indices for (int n = 0; n < n_PC - n_pc_wm; n++) { Q_PC_N[n] = Q_I_N[n]; } + + // A number of n_pc_wm other parity check bits are placed in the bit indices of minimum row weight of most reliable bit indices + if (n_pc_wm == 1) { + // tilde{Q_I}^N = K most reliable indices of bar{Q_I}^N = Q_I_N[n_PC .. K+n_PC-1]. + // Find minimum row weight over this range; on ties, keep the most reliable + // (later position, since Q_I_N is ascending by reliability). + int min_weight = INT_MAX; + int best_pos = -1; + for (int n = n_PC; n <= (K + n_PC) - 1; n++) { + const int w = __builtin_popcount((unsigned)Q_I_N[n]); + if (w <= min_weight) { + min_weight = w; + best_pos = n; + } + } + AssertFatal(best_pos >= 0, "No candidate found in tilde{Q_I}^N for n_pc_wm = 1 (K=%u, n_PC=%u)", K, n_PC); + Q_PC_N[n_PC - 1] = Q_I_N[best_pos]; + } } // Q_F_N = Q_0_N-1 \ Q_I_N @@ -266,7 +287,7 @@ void nr_polar_info_bit_pattern(uint8_t *ibp, } } - // Information and Parity Chack Bit Pattern + // Information and Parity Check Bit Pattern for (int n = 0; n <= N - 1; n++) { ibp[n] = 0; for (int m = 0; m <= (K + n_PC) - 1; m++) { @@ -277,7 +298,7 @@ void nr_polar_info_bit_pattern(uint8_t *ibp, } pcbp[n] = 0; - for (int m = 0; m < n_PC - n_pc_wm; m++) { + for (int m = 0; m < n_PC; m++) { if (n == Q_PC_N[m]) { pcbp[n] = 1; break; diff --git a/openair1/PHY/NR_TRANSPORT/nr_antenna_control.c b/openair1/PHY/NR_TRANSPORT/nr_antenna_control.c index fc9fa02e98..4a4bac3f17 100644 --- a/openair1/PHY/NR_TRANSPORT/nr_antenna_control.c +++ b/openair1/PHY/NR_TRANSPORT/nr_antenna_control.c @@ -56,6 +56,10 @@ static struct { int port; nr_antenna_ctrl_backend_t backend; int64_t tx_lock_until_ms[NR_MAX_ANTENNAS]; + pthread_mutex_t command_lock; + pthread_mutex_t clients_lock; + pthread_cond_t clients_done; + unsigned int client_count; #ifdef OAI_MPLANE ru_session_t *ru_session; #else @@ -191,8 +195,8 @@ static void process_command(const char *cmd, char *response, size_t resp_size) 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; + uint8_t old_requested = nr_get_requested_tx_antenna_bitmask(g_ctrl.ant_state); + uint8_t waking = requested & ~old_requested; 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, @@ -201,8 +205,8 @@ static void process_command(const char *cmd, char *response, size_t resp_size) } } - nr_set_tx_antenna_bitmask(g_ctrl.ant_state, requested); - uint8_t muted = old & ~requested; + nr_request_tx_antenna_bitmask(g_ctrl.ant_state, requested); + uint8_t muted = old_requested & ~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) @@ -210,9 +214,13 @@ static void process_command(const char *cmd, char *response, size_t resp_size) if (waking & bit) set_tx_lock(ant_id, 0); } + bool pending = nr_get_pending_tx_antenna_bitmask(g_ctrl.ant_state, NULL); + uint8_t active = nr_get_tx_antenna_bitmask(g_ctrl.ant_state); snprintf(response, resp_size, - "OK: TX mask 0x%02X -> 0x%02X (TSM=%ds%s)\n", - old, requested, duration, force ? ", emergency override" : ""); + "OK: TX mask 0x%02X -> 0x%02X (active=0x%02X%s, TSM=%ds%s)\n", + old_requested, requested, active, + pending ? ", HARQ drain pending" : "", + duration, force ? ", emergency override" : ""); } else if (strncmp(p, "SET RX MASK ", 12) == 0) { unsigned int mask; @@ -225,11 +233,15 @@ static void process_command(const char *cmd, char *response, size_t resp_size) } else if (strcmp(p, "STATUS") == 0) { bool in_recovery = nr_is_in_recovery_mode(g_ctrl.ant_state); + uint8_t requested_mask = nr_get_requested_tx_antenna_bitmask(g_ctrl.ant_state); + bool pending = nr_get_pending_tx_antenna_bitmask(g_ctrl.ant_state, NULL); snprintf(response, resp_size, "=== Antenna Status ===\n" "TX Array: %s\n" " Antennas: %d configured, %d active\n" " Bitmask: 0x%02X\n" + " Requested bitmask: 0x%02X\n" + " HARQ drain pending: %s\n" " Antenna 0 protected: %s\n" "RX Array: %s\n" " Antennas: %d configured, %d active\n" @@ -240,6 +252,8 @@ static void process_command(const char *cmd, char *response, size_t resp_size) 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), + requested_mask, + pending ? "YES" : "NO", g_ctrl.ant_state->config.protect_antenna0 ? "YES" : "NO", g_ctrl.ant_state->rx_array_name, g_ctrl.ant_state->num_rx_antennas, @@ -332,6 +346,13 @@ static void process_command(const char *cmd, char *response, size_t resp_size) } } +static void send_client_prompt(int client_fd) +{ + const char prompt[] = "> "; + if (send(client_fd, prompt, sizeof(prompt) - 1, MSG_NOSIGNAL) < 0) + LOG_D(PHY, "[ANTENNA_CTRL] Failed to send client prompt: %s\n", strerror(errno)); +} + // Handle a single client connection static void handle_client(int client_fd) { @@ -344,24 +365,28 @@ static void handle_client(int client_fd) "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); + send(client_fd, welcome, strlen(welcome), MSG_NOSIGNAL); + send_client_prompt(client_fd); 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; + if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)) + continue; + if (n <= 0) + break; buffer[n] = '\0'; - // Process command + // Serialize state-changing commands while allowing multiple connected + // clients (for example the persistent O1 bridge and a diagnostic client). + pthread_mutex_lock(&g_ctrl.command_lock); process_command(buffer, response, sizeof(response)); + pthread_mutex_unlock(&g_ctrl.command_lock); // Send response if (strlen(response) > 0) { - send(client_fd, response, strlen(response), 0); + send(client_fd, response, strlen(response), MSG_NOSIGNAL); } // Check for quit @@ -369,12 +394,26 @@ static void handle_client(int client_fd) strstr(buffer, "EXIT") || strstr(buffer, "exit")) { break; } + send_client_prompt(client_fd); } close(client_fd); LOG_I(PHY, "[ANTENNA_CTRL] Client disconnected\n"); } +static void *client_thread(void *arg) +{ + const int client_fd = (int)(intptr_t)arg; + handle_client(client_fd); + + pthread_mutex_lock(&g_ctrl.clients_lock); + AssertFatal(g_ctrl.client_count > 0, "Antenna-control client count underflow\n"); + g_ctrl.client_count--; + pthread_cond_broadcast(&g_ctrl.clients_done); + pthread_mutex_unlock(&g_ctrl.clients_lock); + return NULL; +} + // Server thread function static void *server_thread(void *arg) { @@ -401,8 +440,27 @@ static void *server_thread(void *arg) 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); + + // Bound recv() so shutdown can wait for detached client workers without + // leaving a thread blocked forever on an idle persistent connection. + struct timeval client_timeout = {.tv_sec = 1, .tv_usec = 0}; + setsockopt(client_fd, SOL_SOCKET, SO_RCVTIMEO, &client_timeout, sizeof(client_timeout)); + + pthread_mutex_lock(&g_ctrl.clients_lock); + g_ctrl.client_count++; + pthread_mutex_unlock(&g_ctrl.clients_lock); + + pthread_t client; + if (pthread_create(&client, NULL, client_thread, (void *)(intptr_t)client_fd) != 0) { + LOG_E(PHY, "[ANTENNA_CTRL] Failed to create client thread\n"); + close(client_fd); + pthread_mutex_lock(&g_ctrl.clients_lock); + g_ctrl.client_count--; + pthread_cond_broadcast(&g_ctrl.clients_done); + pthread_mutex_unlock(&g_ctrl.clients_lock); + continue; + } + pthread_detach(client); } return NULL; @@ -469,11 +527,18 @@ bool nr_antenna_control_start_test_server(nr_antenna_state_t *ant_state, int por 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; + pthread_mutex_init(&g_ctrl.command_lock, NULL); + pthread_mutex_init(&g_ctrl.clients_lock, NULL); + pthread_cond_init(&g_ctrl.clients_done, NULL); + g_ctrl.client_count = 0; // 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)); + pthread_cond_destroy(&g_ctrl.clients_done); + pthread_mutex_destroy(&g_ctrl.clients_lock); + pthread_mutex_destroy(&g_ctrl.command_lock); return false; } @@ -491,6 +556,9 @@ bool nr_antenna_control_start_test_server(nr_antenna_state_t *ant_state, int por 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); + pthread_cond_destroy(&g_ctrl.clients_done); + pthread_mutex_destroy(&g_ctrl.clients_lock); + pthread_mutex_destroy(&g_ctrl.command_lock); return false; } @@ -498,6 +566,9 @@ bool nr_antenna_control_start_test_server(nr_antenna_state_t *ant_state, int por 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); + pthread_cond_destroy(&g_ctrl.clients_done); + pthread_mutex_destroy(&g_ctrl.clients_lock); + pthread_mutex_destroy(&g_ctrl.command_lock); return false; } @@ -507,6 +578,9 @@ bool nr_antenna_control_start_test_server(nr_antenna_state_t *ant_state, int por LOG_E(PHY, "[ANTENNA_CTRL] Failed to create server thread\n"); g_ctrl.running = false; close(g_ctrl.server_fd); + pthread_cond_destroy(&g_ctrl.clients_done); + pthread_mutex_destroy(&g_ctrl.clients_lock); + pthread_mutex_destroy(&g_ctrl.command_lock); return false; } @@ -540,6 +614,16 @@ void nr_antenna_control_stop(void) // Wait for thread pthread_join(g_ctrl.thread, NULL); + + // Client workers wake from recv() at least once per second and observe + // running=false. Keep ant_state valid until every worker has exited. + pthread_mutex_lock(&g_ctrl.clients_lock); + while (g_ctrl.client_count > 0) + pthread_cond_wait(&g_ctrl.clients_done, &g_ctrl.clients_lock); + pthread_mutex_unlock(&g_ctrl.clients_lock); + pthread_cond_destroy(&g_ctrl.clients_done); + pthread_mutex_destroy(&g_ctrl.clients_lock); + pthread_mutex_destroy(&g_ctrl.command_lock); } g_ctrl.backend = ANTENNA_CTRL_BACKEND_NONE; diff --git a/openair1/PHY/NR_TRANSPORT/nr_antenna_state.c b/openair1/PHY/NR_TRANSPORT/nr_antenna_state.c index 1c5a3df415..473be03875 100644 --- a/openair1/PHY/NR_TRANSPORT/nr_antenna_state.c +++ b/openair1/PHY/NR_TRANSPORT/nr_antenna_state.c @@ -36,6 +36,8 @@ void nr_init_antenna_state(nr_antenna_state_t *state, // 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->requested_tx_antenna_bitmask = state->tx_antenna_bitmask; + state->tx_mask_update_pending = false; state->rx_antenna_bitmask = (1 << state->num_rx_antennas) - 1; // Set O-RAN array names @@ -76,33 +78,103 @@ void nr_free_antenna_state(nr_antenna_state_t *state) pthread_mutex_destroy(&state->lock); } +static uint8_t valid_tx_mask(const nr_antenna_state_t *state) +{ + return (uint8_t)((1u << state->num_tx_antennas) - 1u); +} + +/* Caller holds state->lock. */ +static void set_tx_antenna_bitmask_locked(nr_antenna_state_t *state, uint8_t bitmask) +{ + const uint8_t old_bitmask = state->tx_antenna_bitmask; + state->tx_antenna_bitmask = bitmask & valid_tx_mask(state); + + if (state->tx_antenna_bitmask == old_bitmask) + return; + + 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); +} + 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); - } - + const uint8_t old_bitmask = state->tx_antenna_bitmask; + state->requested_tx_antenna_bitmask = bitmask & valid_tx_mask(state); + state->tx_mask_update_pending = false; + set_tx_antenna_bitmask_locked(state, state->requested_tx_antenna_bitmask); pthread_mutex_unlock(&state->lock); - return old_bitmask; } +uint8_t nr_request_tx_antenna_bitmask(nr_antenna_state_t *state, uint8_t bitmask) +{ + if (state == NULL) return 0; + + pthread_mutex_lock(&state->lock); + const uint8_t old_requested = state->requested_tx_antenna_bitmask; + const uint8_t requested = bitmask & valid_tx_mask(state); + state->requested_tx_antenna_bitmask = requested; + + /* Adding chains cannot invalidate an in-flight PDSCH. Removing a chain can, + * so leave the physical mask unchanged until MAC has drained HARQ processes + * whose original layer count exceeds the new target. */ + if ((state->tx_antenna_bitmask & (uint8_t)~requested) == 0) { + state->tx_mask_update_pending = false; + set_tx_antenna_bitmask_locked(state, requested); + } else { + state->tx_mask_update_pending = true; + LOG_I(PHY, + "[ANTENNA_STATE] TX downshift requested: active=0x%02X target=0x%02X; waiting for DL HARQ drain\n", + state->tx_antenna_bitmask, + requested); + } + pthread_mutex_unlock(&state->lock); + return old_requested; +} + +uint8_t nr_get_requested_tx_antenna_bitmask(const nr_antenna_state_t *state) +{ + if (state == NULL) return 0xFF; + pthread_mutex_lock((pthread_mutex_t *)&state->lock); + const uint8_t requested = state->requested_tx_antenna_bitmask; + pthread_mutex_unlock((pthread_mutex_t *)&state->lock); + return requested; +} + +bool nr_get_pending_tx_antenna_bitmask(const nr_antenna_state_t *state, uint8_t *bitmask) +{ + if (state == NULL) return false; + pthread_mutex_lock((pthread_mutex_t *)&state->lock); + const bool pending = state->tx_mask_update_pending; + if (pending && bitmask != NULL) + *bitmask = state->requested_tx_antenna_bitmask; + pthread_mutex_unlock((pthread_mutex_t *)&state->lock); + return pending; +} + +bool nr_apply_requested_tx_antenna_bitmask(nr_antenna_state_t *state, uint8_t expected_bitmask) +{ + if (state == NULL) return false; + pthread_mutex_lock(&state->lock); + const bool matches = state->tx_mask_update_pending + && state->requested_tx_antenna_bitmask == expected_bitmask; + if (matches) { + set_tx_antenna_bitmask_locked(state, expected_bitmask); + state->tx_mask_update_pending = false; + } + pthread_mutex_unlock(&state->lock); + return matches; +} + uint8_t nr_set_rx_antenna_bitmask(nr_antenna_state_t *state, uint8_t bitmask) { if (state == NULL) return 0; @@ -183,16 +255,16 @@ void nr_mute_tx_antenna(nr_antenna_state_t *state, uint8_t ant_id) return; } - uint8_t new_mask = state->tx_antenna_bitmask & ~(1 << ant_id); - nr_set_tx_antenna_bitmask(state, new_mask); + uint8_t new_mask = nr_get_requested_tx_antenna_bitmask(state) & ~(1 << ant_id); + nr_request_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); + uint8_t new_mask = nr_get_requested_tx_antenna_bitmask(state) | (1 << ant_id); + nr_request_tx_antenna_bitmask(state, new_mask); } void nr_mute_rx_antenna(nr_antenna_state_t *state, uint8_t ant_id) @@ -216,16 +288,12 @@ int nr_get_effective_mimo_layers(const nr_antenna_state_t *state, int requested_ 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; + /* Cap new transmissions to the target immediately. During a downshift the + * physical mask can remain wider for a few HARQ rounds, which is safe; the + * reverse ordering would erase layers from buffered retransmissions. */ + const int target = popcount8(nr_get_requested_tx_antenna_bitmask(state)); + if (target > 0 && target < requested_layers) + return target; return requested_layers; } @@ -296,8 +364,10 @@ void nr_unmute_all_tx_antennas(nr_antenna_state_t *state) uint8_t old_mask = state->tx_antenna_bitmask; uint8_t all_on = (1 << state->num_tx_antennas) - 1; + state->requested_tx_antenna_bitmask = all_on; + state->tx_mask_update_pending = false; state->tx_antenna_bitmask = all_on; - + // Enter recovery mode if we were muted if (old_mask != all_on) { state->recovery.in_recovery = true; diff --git a/openair1/PHY/NR_TRANSPORT/nr_antenna_state.h b/openair1/PHY/NR_TRANSPORT/nr_antenna_state.h index eb810a8e2b..a2e348211b 100644 --- a/openair1/PHY/NR_TRANSPORT/nr_antenna_state.h +++ b/openair1/PHY/NR_TRANSPORT/nr_antenna_state.h @@ -19,6 +19,12 @@ typedef struct { /// Corresponds to o-ran-uplane-conf:antenna-bitmask uint8_t tx_antenna_bitmask; uint8_t rx_antenna_bitmask; + + /// Target mask used for new scheduling while an unsafe TX downshift drains. + uint8_t requested_tx_antenna_bitmask; + /// True when requested_tx_antenna_bitmask removes physical TX chains that + /// cannot be muted until incompatible in-flight DL HARQ processes complete. + bool tx_mask_update_pending; /// Total number of configured antennas uint8_t num_tx_antennas; @@ -86,6 +92,45 @@ void nr_free_antenna_state(nr_antenna_state_t *state); */ uint8_t nr_set_tx_antenna_bitmask(nr_antenna_state_t *state, uint8_t bitmask); +/** + * @brief Request a TX mask, staging physical downshifts until MAC HARQ is safe + * + * New scheduling immediately observes the requested mask. Pure upshifts are + * applied physically at once; a request that removes active TX chains remains + * pending until the MAC scheduler explicitly commits it. + * + * @param state Pointer to antenna state structure + * @param bitmask Requested antenna bitmask + * @return Previous requested bitmask + */ +uint8_t nr_request_tx_antenna_bitmask(nr_antenna_state_t *state, uint8_t bitmask); + +/** + * @brief Read the requested TX mask + * @return Requested bitmask, or 0xFF when state is NULL + */ +uint8_t nr_get_requested_tx_antenna_bitmask(const nr_antenna_state_t *state); + +/** + * @brief Atomically snapshot a pending TX-mask request + * @param state Pointer to antenna state structure + * @param bitmask Output requested mask when pending + * @return true when a physical mask update is pending + */ +bool nr_get_pending_tx_antenna_bitmask(const nr_antenna_state_t *state, uint8_t *bitmask); + +/** + * @brief Commit a previously checked pending TX mask + * + * The compare against expected_bitmask prevents a concurrent control request + * from being applied using a stale HARQ-safety decision. + * + * @param state Pointer to antenna state structure + * @param expected_bitmask Mask for which the scheduler completed its check + * @return true when that exact pending request was committed + */ +bool nr_apply_requested_tx_antenna_bitmask(nr_antenna_state_t *state, uint8_t expected_bitmask); + /** * @brief Set RX antenna bitmask (O-RAN O1 interface equivalent) * @param state Pointer to antenna state structure @@ -169,8 +214,9 @@ 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). + * For MIMO, the number of layers is limited by the requested antenna mask. + * During a staged downshift this stops new high-rank HARQ traffic before the + * physical antenna mask changes. * * @param state Pointer to antenna state structure * @param requested_layers Originally requested number of layers diff --git a/openair1/PHY/NR_UE_TRANSPORT/csi_rx.c b/openair1/PHY/NR_UE_TRANSPORT/csi_rx.c index c0b87ec563..82cca94ddd 100644 --- a/openair1/PHY/NR_UE_TRANSPORT/csi_rx.c +++ b/openair1/PHY/NR_UE_TRANSPORT/csi_rx.c @@ -16,6 +16,8 @@ #include #include #include +#include +#include #include "executables/nr-softmodem-common.h" #include "nr_transport_proto_ue.h" @@ -24,9 +26,8 @@ #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. +// spatial-spectrum telemetry. MAX_ANT and the local PDSCH rank ceiling are +// eight on this branch. // Sorts eigenvalues in descending order on output. static void nr_hermitian_eigenvalues(const int N, float A_re[MAX_ANT][MAX_ANT], @@ -165,12 +166,91 @@ typedef struct { int samples; int dimensions; float eig_rel_db[NR_MAX_NB_LAYERS]; + float rank_capacity[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; +typedef enum { + NR_CSI_RANK_CAPACITY, + NR_CSI_RANK_LEGACY, + NR_CSI_RANK_FIXED, +} nr_csi_rank_policy_t; + +typedef struct { + nr_csi_rank_policy_t policy; + uint8_t fixed_rank; + float capacity_margin; +} nr_csi_rank_config_t; + +static pthread_once_t csi_rank_config_once = PTHREAD_ONCE_INIT; +static nr_csi_rank_config_t csi_rank_config = { + .policy = NR_CSI_RANK_CAPACITY, + .fixed_rank = 1, + .capacity_margin = 0.05f, +}; + +static const char *nr_csi_rank_policy_name(const nr_csi_rank_config_t *config) +{ + if (config->policy == NR_CSI_RANK_LEGACY) + return "legacy"; + if (config->policy == NR_CSI_RANK_FIXED) + return "fixed"; + return "capacity"; +} + +static void configure_csi_rank_policy(void) +{ + const char *value = getenv("OAI_CSI_RANK_POLICY"); + if (value != NULL && strcmp(value, "legacy") == 0) { + csi_rank_config.policy = NR_CSI_RANK_LEGACY; + } else if (value == NULL || strcmp(value, "capacity") == 0) { + csi_rank_config.policy = NR_CSI_RANK_CAPACITY; + } else if (strncmp(value, "fixed", 5) == 0) { + const char *rank_text = value + 5; + if (*rank_text == ':') + rank_text++; + const long rank = strtol(rank_text, NULL, 10); + if (rank >= 1 && rank <= NR_MAX_NB_LAYERS) { + csi_rank_config.policy = NR_CSI_RANK_FIXED; + csi_rank_config.fixed_rank = rank; + } else { + LOG_W(NR_PHY, + "OAI_CSI_RANK_POLICY=%s has invalid fixed rank; using capacity\n", + value); + } + } else { + LOG_W(NR_PHY, + "Unknown OAI_CSI_RANK_POLICY=%s; using capacity\n", + value); + } + + const char *margin_text = getenv("OAI_CSI_RANK_MARGIN"); + if (margin_text != NULL) { + const float margin = strtof(margin_text, NULL); + if (isfinite(margin) && margin >= 0.0f && margin <= 1.0f) + csi_rank_config.capacity_margin = margin; + else + LOG_W(NR_PHY, + "OAI_CSI_RANK_MARGIN must be in [0,1]; using %.2f\n", + csi_rank_config.capacity_margin); + } + + LOG_I(NR_PHY, + "[CSI_RANK] policy=%s fixed_rank=%u capacity_margin=%.3f\n", + nr_csi_rank_policy_name(&csi_rank_config), + csi_rank_config.fixed_rank, + csi_rank_config.capacity_margin); +} + +static const nr_csi_rank_config_t *get_csi_rank_config(void) +{ + pthread_once(&csi_rank_config_once, configure_csi_rank_policy); + return &csi_rank_config; +} + void nr_squared_matrix_element(int32_t *a, int32_t *a_sq, const unsigned short nb_rb) { @@ -527,10 +607,12 @@ 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, + const uint32_t noise_power, uint8_t *rank_indicator, nr_csi_muting_summary_t *muting_summary) { const NR_DL_FRAME_PARMS *frame_parms = &ue->frame_parms; + const nr_csi_rank_config_t *rank_config = get_csi_rank_config(); const int16_t cond_dB_threshold = 5; int count = 0; *rank_indicator = 0; @@ -555,12 +637,15 @@ static int nr_csi_rs_ri_estimation(const PHY_VARS_NR_UE *ue, 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)) { + // Legacy 2x2 keeps the existing condition-number path below. Capacity and + // fixed policies use the same eigen-spectrum path for every MIMO size. + const bool legacy_2x2 = rank_config->policy == NR_CSI_RANK_LEGACY + && ue->frame_parms.nb_antennas_rx == 2 && N_ports == 2; + if (!legacy_2x2) { int rank_count[NR_MAX_NB_LAYERS] = {0}; double eig_sum[MAX_ANT] = {0}; double port_power[MAX_ANT] = {0}; + double capacity_sum[NR_MAX_NB_LAYERS] = {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++) { @@ -587,6 +672,10 @@ static int nr_csi_rs_ri_estimation(const PHY_VARS_NR_UE *ue, float eig[MAX_ANT] = {0}; nr_hermitian_eigenvalues(N_ports, A_re, A_im, eig); if (eig[0] < 1e-6f) continue; + float rank_capacity[NR_MAX_NB_LAYERS] = {0}; + nr_pdsch_rank_capacity_scores(eig, max_rank, (float)noise_power, rank_capacity); + for (int r = 0; r < max_rank; r++) + capacity_sum[r] += rank_capacity[r]; rank_count[0]++; for (int r = 0; r < N_ports; r++) eig_sum[r] += eig[r]; @@ -601,11 +690,23 @@ static int nr_csi_rs_ri_estimation(const PHY_VARS_NR_UE *ue, } 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; + float mean_capacity[NR_MAX_NB_LAYERS] = {0}; + for (int r = 0; r < max_rank; r++) + mean_capacity[r] = capacity_sum[r] / total_sc; + if (rank_config->policy == NR_CSI_RANK_LEGACY) { + for (int r = max_rank - 1; r >= 1; r--) { + if (rank_count[r] > total_sc / 2) { + *rank_indicator = r; + break; + } } + } else if (rank_config->policy == NR_CSI_RANK_FIXED) { + *rank_indicator = min(rank_config->fixed_rank, max_rank) - 1; + } else { + *rank_indicator = nr_pdsch_select_rank(mean_capacity, + max_rank, + rank_config->capacity_margin) + - 1; } if (muting_summary != NULL) { muting_summary->samples = total_sc; @@ -615,6 +716,7 @@ static int nr_csi_rs_ri_estimation(const PHY_VARS_NR_UE *ue, r < physical_rank && eig_sum[r] > 0.0 && eig_sum[0] > 0.0 ? 10.0f * log10f((float)(eig_sum[r] / eig_sum[0])) : -INFINITY; + muting_summary->rank_capacity[r] = r < max_rank ? mean_capacity[r] : 0.0f; } double full_power = 0.0; for (int p = 0; p < N_ports; p++) @@ -753,13 +855,16 @@ static int nr_csi_rs_pmi_estimation(const PHY_VARS_NR_UE *ue, const uint32_t interference_plus_noise_power, const uint8_t rank_indicator, const int16_t log2_re, + uint16_t *pmi_x1, uint8_t *i2, 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. + // rank path finite, including the eight-port codebook search below. const uint32_t non_zero_noise_power = interference_plus_noise_power == 0 ? 1 : interference_plus_noise_power; + *pmi_x1 = 0; + i2[0] = 0; // 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}. @@ -796,7 +901,7 @@ static int nr_csi_rs_pmi_estimation(const PHY_VARS_NR_UE *ue, return 0; } - if(rank_indicator == 0 || rank_indicator == 1) { + if ((rank_indicator == 0 || rank_indicator == 1) && N_ports != 8) { c64_t sum[4] = {0}; c64_t sum2[4] = {0}; int64_t tested_precoded_sinr[4] = {0}; @@ -857,12 +962,131 @@ 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). + } else if (rank_indicator < NR_MAX_NB_LAYERS && rank_indicator < N_ports && N_ports == 8 + && rank_indicator != 2 && rank_indicator != 3 + && csirs_config_pdu->codebook_n1 == 4 + && csirs_config_pdu->codebook_n2 == 1 + && csirs_config_pdu->codebook_mode == 1) { + const int nlayers = rank_indicator + 1; + float gram_re[MAX_ANT][MAX_ANT] = {{0}}; + float gram_im[MAX_ANT][MAX_ANT] = {{0}}; + int samples = 0; + const int sc_stride = 4; + 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; + const uint16_t k = rb * NR_NB_SC_PER_RB; + for (int sc = 0; sc < NR_NB_SC_PER_RB; sc += sc_stride) { + for (int rx = 0; rx < frame_parms->nb_antennas_rx; rx++) { + for (int p = 0; p < N_ports; p++) { + const c16_t hp = csi_rs_estimated_channel_freq[rx][p][k + sc]; + for (int q = 0; q < N_ports; q++) { + const c16_t hq = csi_rs_estimated_channel_freq[rx][q][k + sc]; + gram_re[p][q] += (float)hp.r * hq.r + (float)hp.i * hq.i; + gram_im[p][q] += -(float)hp.i * hq.r + (float)hp.r * hq.i; + } + } + } + samples++; + } + } + + float best_score = -INFINITY; + uint8_t best_i11 = 0; + uint8_t best_i13 = 0; + uint8_t best_i2 = 0; + const uint8_t i11_count = nr_type1_sp_4x1_i11_count(nlayers); + const uint8_t i13_count = nr_type1_sp_4x1_i13_count(nlayers); + const uint8_t i2_count = nr_type1_sp_4x1_i2_count(nlayers); + for (uint8_t candidate_i11 = 0; candidate_i11 < i11_count; candidate_i11++) { + for (uint8_t candidate_i13 = 0; candidate_i13 < i13_count; candidate_i13++) { + for (uint8_t candidate_i2 = 0; candidate_i2 < i2_count; candidate_i2++) { + c16_t weights[NR_MAX_NB_LAYERS][NR_MAX_CSI_PORTS]; + AssertFatal(nr_type1_sp_4x1_weights(nlayers, candidate_i11, candidate_i13, candidate_i2, weights), + "Invalid rank-%d PMI candidate i11=%u i13=%u i2=%u\n", + nlayers, + candidate_i11, + candidate_i13, + candidate_i2); + float precoded_re[MAX_ANT][MAX_ANT] = {{0}}; + float precoded_im[MAX_ANT][MAX_ANT] = {{0}}; + for (int a = 0; a < nlayers; a++) { + for (int b = 0; b < nlayers; b++) { + double value_re = 0.0; + double value_im = 0.0; + for (int p = 0; p < N_ports; p++) { + const double wa_re = weights[a][p].r / (double)SHRT_MAX; + const double wa_im = weights[a][p].i / (double)SHRT_MAX; + for (int q = 0; q < N_ports; q++) { + const double wb_re = weights[b][q].r / (double)SHRT_MAX; + const double wb_im = weights[b][q].i / (double)SHRT_MAX; + const double left_re = wa_re * gram_re[p][q] + wa_im * gram_im[p][q]; + const double left_im = wa_re * gram_im[p][q] - wa_im * gram_re[p][q]; + value_re += left_re * wb_re - left_im * wb_im; + value_im += left_re * wb_im + left_im * wb_re; + } + } + precoded_re[a][b] = value_re; + precoded_im[a][b] = value_im; + } + } + // Remove accumulated floating-point asymmetry before Jacobi sweeps. + for (int a = 0; a < nlayers; a++) { + precoded_im[a][a] = 0.0f; + for (int b = a + 1; b < nlayers; b++) { + const float avg_re = 0.5f * (precoded_re[a][b] + precoded_re[b][a]); + const float avg_im = 0.5f * (precoded_im[a][b] - precoded_im[b][a]); + precoded_re[a][b] = precoded_re[b][a] = avg_re; + precoded_im[a][b] = avg_im; + precoded_im[b][a] = -avg_im; + } + } + float eigenvalues[MAX_ANT] = {0}; + nr_hermitian_eigenvalues(nlayers, precoded_re, precoded_im, eigenvalues); + float score = 0.0f; + if (samples > 0) + for (int layer = 0; layer < nlayers; layer++) + score += log2f(1.0f + eigenvalues[layer] / (samples * (float)non_zero_noise_power)); + const float tie_tolerance = fmaxf(1e-4f, fabsf(best_score) * 1e-5f); + if (!isfinite(best_score) || score > best_score + tie_tolerance) { + best_score = score; + best_i11 = candidate_i11; + best_i13 = candidate_i13; + best_i2 = candidate_i2; + } + } + } + } + + // x1 is packed i11|i12|i13. N2=1 makes i12 zero length; i13 is + // two bits for ranks 2--4 and absent for the other ranks. + const uint8_t i13_bitlen = i13_count > 1 ? 2 : 0; + *pmi_x1 = (best_i11 << i13_bitlen) | best_i13; + i2[0] = best_i2; + const float equivalent_sinr = best_score > 0.0f ? exp2f(best_score / nlayers) - 1.0f : 0.0f; + const float equivalent_sinr_dB = equivalent_sinr > 0.0f ? 10.0f * log10f(equivalent_sinr) : 0.0f; + *precoded_sinr_dB = lroundf(fmaxf(0.0f, fminf(equivalent_sinr_dB, INT_MAX))); + + const char *pmi_telemetry = getenv("OAI_CSI_PMI_TELEMETRY"); + if (pmi_telemetry != NULL && strcmp(pmi_telemetry, "1") == 0) + LOG_I(NR_PHY, + "[CSI_PMI] ports=8 geometry=4x1 mode=1 rank=%d i11=%u i13=%u i2=%u candidates=%u " + "capacity=%.4f equivalent_sinr_db=%.2f samples=%d\n", + nlayers, + best_i11, + best_i13, + best_i2, + i11_count * i13_count * i2_count, + best_score, + equivalent_sinr_dB, + samples); + } else if (rank_indicator >= 2 && rank_indicator < NR_MAX_NB_LAYERS + && rank_indicator < N_ports) { + // Unsupported geometries retain the prior identity-like CQI fallback. + // The eight-port rank-3/4 receive path also remains identity: OAI's + // legacy detector cannot yet decode a non-identity 8-port rank-3/4 PDSCH. int nlayers = rank_indicator + 1; int64_t total_power = 0; for (int rb = csirs_config_pdu->start_rb; @@ -877,7 +1101,6 @@ static int nr_csi_rs_pmi_estimation(const PHY_VARS_NR_UE *ue, } } } - i2[0] = 0; if (total_power > 0) *precoded_sinr_dB = dB_fixed((total_power / nlayers) / non_zero_noise_power); } else { @@ -1128,11 +1351,12 @@ void nr_ue_csi_rs_procedures(PHY_VARS_NR_UE *ue, mapping_parms.ports, csi_rs_estimated_channel_freq, log2_maxh, + noise_power, &rank_indicator, &muting_summary); } - uint8_t i1[3] = {0}; + uint16_t i1 = 0; uint8_t i2[1] = {0}; uint8_t cqi = 0; int32_t precoded_sinr_dB = 0; @@ -1145,6 +1369,7 @@ void nr_ue_csi_rs_procedures(PHY_VARS_NR_UE *ue, csi_info->csi_im_meas_computed ? csi_info->interference_plus_noise_power : noise_power, rank_indicator, log2_re, + &i1, i2, &precoded_sinr_dB); @@ -1155,9 +1380,12 @@ void nr_ue_csi_rs_procedures(PHY_VARS_NR_UE *ue, const char *muting_telemetry = getenv("OAI_CSI_MUTING_TELEMETRY"); if (muting_summary.samples > 0 && muting_telemetry != NULL && strcmp(muting_telemetry, "1") == 0) { + const nr_csi_rank_config_t *rank_config = get_csi_rank_config(); 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 " + "rank_policy=%s ri=%u cqi=%u sinr_db=%d " + "eig_rel_db=%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f " + "rank_capacity=%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f " "prefix_rel_db=%.2f,%.2f,%.2f,%.2f\n", proc->frame_rx, proc->nr_slot_rx, @@ -1165,6 +1393,7 @@ void nr_ue_csi_rs_procedures(PHY_VARS_NR_UE *ue, frame_parms->nb_antennas_rx, mapping_parms.ports, muting_summary.samples, + nr_csi_rank_policy_name(rank_config), rank_indicator + 1, cqi, precoded_sinr_dB, @@ -1172,6 +1401,18 @@ void nr_ue_csi_rs_procedures(PHY_VARS_NR_UE *ue, muting_summary.eig_rel_db[1], muting_summary.eig_rel_db[2], muting_summary.eig_rel_db[3], + muting_summary.eig_rel_db[4], + muting_summary.eig_rel_db[5], + muting_summary.eig_rel_db[6], + muting_summary.eig_rel_db[7], + muting_summary.rank_capacity[0], + muting_summary.rank_capacity[1], + muting_summary.rank_capacity[2], + muting_summary.rank_capacity[3], + muting_summary.rank_capacity[4], + muting_summary.rank_capacity[5], + muting_summary.rank_capacity[6], + muting_summary.rank_capacity[7], muting_summary.prefix_rel_db[0], muting_summary.prefix_rel_db[1], muting_summary.prefix_rel_db[2], @@ -1183,12 +1424,12 @@ void nr_ue_csi_rs_procedures(PHY_VARS_NR_UE *ue, LOG_I(NR_PHY, "[UE %d] RSRP = %i dBm\n", ue->Mod_id, rsrp_dBm); break; case 26 : - LOG_I(NR_PHY, "RI = %i i1 = %i.%i.%i, i2 = %i, SINR = %i dB, CQI = %i\n", - rank_indicator + 1, i1[0], i1[1], i1[2], i2[0], precoded_sinr_dB, cqi); + LOG_I(NR_PHY, "RI = %i i1 = %u, i2 = %i, SINR = %i dB, CQI = %i\n", + rank_indicator + 1, i1, i2[0], precoded_sinr_dB, cqi); break; case 27 : - LOG_I(NR_PHY, "RSRP = %i dBm, RI = %i i1 = %i.%i.%i, i2 = %i, SINR = %i dB, CQI = %i\n", - rsrp_dBm, rank_indicator + 1, i1[0], i1[1], i1[2], i2[0], precoded_sinr_dB, cqi); + LOG_I(NR_PHY, "RSRP = %i dBm, RI = %i i1 = %u, i2 = %i, SINR = %i dB, CQI = %i\n", + rsrp_dBm, rank_indicator + 1, i1, i2[0], precoded_sinr_dB, cqi); break; default : AssertFatal(false, "Not supported measurement configuration\n"); @@ -1205,7 +1446,7 @@ void nr_ue_csi_rs_procedures(PHY_VARS_NR_UE *ue, .is_neighboring_cell = false, .rsrp_dBm = rsrp_dBm, .rank_indicator = rank_indicator, - .i1 = *i1, + .i1 = i1, .i2 = *i2, .cqi = cqi, .radiolink_monitoring = RLM_no_monitoring, // TODO do be activated in case of RLM based on CSI-RS diff --git a/openair1/PHY/NR_UE_TRANSPORT/nr_dlsch_demodulation.c b/openair1/PHY/NR_UE_TRANSPORT/nr_dlsch_demodulation.c index 43e6ed693a..3b2067d599 100644 --- a/openair1/PHY/NR_UE_TRANSPORT/nr_dlsch_demodulation.c +++ b/openair1/PHY/NR_UE_TRANSPORT/nr_dlsch_demodulation.c @@ -18,6 +18,7 @@ #include "PHY/NR_REFSIG/dmrs_nr.h" #include "common/utils/nr/nr_common.h" #include +#include #include "openair1/PHY/TOOLS/phy_scope_interface.h" #include "nfapi/open-nFAPI/nfapi/public_inc/nfapi_nr_interface.h" @@ -863,15 +864,16 @@ static void nr_dlsch_mmse(uint32_t rx_size_symbol, * 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) +static int nr_dlsch_mmse_high_rank_reference(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 first_re, + 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) { @@ -882,7 +884,7 @@ static int nr_dlsch_mmse_high_rank(uint32_t rx_size_symbol, const double q15 = 32768.0; const double normalized_noise = (double)noise_var / (q15 * q15); - for (int re = 0; re < length; re++) { + for (int re = first_re; re < first_re + length; re++) { double complex gram[NR_MAX_NB_LAYERS][NR_MAX_NB_LAYERS] = {{0}}; double complex rhs[NR_MAX_NB_LAYERS] = {0}; @@ -978,6 +980,469 @@ static int nr_dlsch_mmse_high_rank(uint32_t rx_size_symbol, return 0; } +typedef enum { + NR_HIGH_RANK_CHOLESKY, + NR_HIGH_RANK_REFERENCE, +} nr_high_rank_detector_t; + +typedef struct { + nr_high_rank_detector_t detector; + int re_group; +} nr_high_rank_detector_config_t; + +static pthread_once_t high_rank_detector_once = PTHREAD_ONCE_INIT; +static nr_high_rank_detector_config_t high_rank_detector_config = { + .detector = NR_HIGH_RANK_CHOLESKY, + .re_group = 1, +}; + +static void configure_high_rank_detector(void) +{ + const char *detector = getenv("OAI_HIGH_RANK_DETECTOR"); + if (detector != NULL && strcmp(detector, "reference") == 0) + high_rank_detector_config.detector = NR_HIGH_RANK_REFERENCE; + else if (detector != NULL && strcmp(detector, "cholesky") != 0) + LOG_W(NR_PHY, + "Unknown OAI_HIGH_RANK_DETECTOR=%s; using cholesky\n", + detector); + + const char *group = getenv("OAI_HIGH_RANK_GROUP"); + if (group != NULL) { + const long parsed = strtol(group, NULL, 10); + if (parsed >= 1 && parsed <= NR_NB_SC_PER_RB) + high_rank_detector_config.re_group = parsed; + else + LOG_W(NR_PHY, + "OAI_HIGH_RANK_GROUP must be in [1,%d]; using 1\n", + NR_NB_SC_PER_RB); + } + + LOG_I(NR_PHY, + "[HIGH_RANK_DETECTOR] mode=%s re_group=%d\n", + high_rank_detector_config.detector == NR_HIGH_RANK_REFERENCE ? "reference" : "cholesky", + high_rank_detector_config.re_group); +} + +static const nr_high_rank_detector_config_t *get_high_rank_detector_config(void) +{ + pthread_once(&high_rank_detector_once, configure_high_rank_detector); + return &high_rank_detector_config; +} + +/* Factor A = L L^H. A is Hermitian positive definite after MMSE + * regularization, so Cholesky is both cheaper and more stable than forming an + * explicit inverse. */ +static bool nr_high_rank_cholesky(uint8_t nl, + const float complex A[NR_MAX_NB_LAYERS][NR_MAX_NB_LAYERS], + float complex L[NR_MAX_NB_LAYERS][NR_MAX_NB_LAYERS]) +{ + memset(L, 0, sizeof(float complex) * NR_MAX_NB_LAYERS * NR_MAX_NB_LAYERS); + for (int row = 0; row < nl; row++) { + for (int col = 0; col <= row; col++) { + float complex value = A[row][col]; + for (int k = 0; k < col; k++) + value -= L[row][k] * conjf(L[col][k]); + if (row == col) { + const float diagonal = crealf(value); + if (!isfinite(diagonal) || diagonal <= 1e-12f) + return false; + L[row][col] = sqrtf(diagonal); + } else { + L[row][col] = value / crealf(L[col][col]); + } + } + } + return true; +} + +static void nr_high_rank_cholesky_solve(uint8_t nl, + const float complex L[NR_MAX_NB_LAYERS][NR_MAX_NB_LAYERS], + const float complex b[NR_MAX_NB_LAYERS], + float complex x[NR_MAX_NB_LAYERS]) +{ + float complex z[NR_MAX_NB_LAYERS] = {0}; + for (int row = 0; row < nl; row++) { + float complex value = b[row]; + for (int col = 0; col < row; col++) + value -= L[row][col] * z[col]; + z[row] = value / crealf(L[row][row]); + } + for (int row = nl - 1; row >= 0; row--) { + float complex value = z[row]; + for (int col = row + 1; col < nl; col++) + value -= conjf(L[col][row]) * x[col]; + x[row] = value / crealf(L[row][row]); + } +} + +static void nr_high_rank_inverse_diagonal(uint8_t nl, + const float complex L[NR_MAX_NB_LAYERS][NR_MAX_NB_LAYERS], + float diagonal[NR_MAX_NB_LAYERS]) +{ + for (int basis = 0; basis < nl; basis++) { + float complex column[NR_MAX_NB_LAYERS] = {0}; + for (int row = 0; row < nl; row++) { + float complex value = row == basis ? 1.0f : 0.0f; + for (int col = 0; col < row; col++) + value -= L[row][col] * column[col]; + column[row] = value / crealf(L[row][row]); + } + diagonal[basis] = 0.0f; + for (int row = basis; row < nl; row++) + diagonal[basis] += crealf(column[row] * conjf(column[row])); + } +} + +/* A permutation-diagonal channel does not need an NxN matrix inversion. This + * is the common RF-simulator case (and also occurs with sufficiently isolated + * physical paths): every layer has one distinct, dominant receive antenna. + * Keep the decision data-driven so correlated channels automatically use the + * full joint detector below. */ +static bool nr_high_rank_permutation_diagonal(uint8_t n_rx, + uint8_t nl, + const float complex h[MAX_ANT][NR_MAX_NB_LAYERS], + int dominant_rx[NR_MAX_NB_LAYERS]) +{ + bool rx_claimed[MAX_ANT] = {false}; + for (int layer = 0; layer < nl; layer++) { + float total_power = 0.0f; + float dominant_power = 0.0f; + int dominant = -1; + for (int aarx = 0; aarx < n_rx; aarx++) { + const float power = crealf(h[aarx][layer] * conjf(h[aarx][layer])); + total_power += power; + if (power > dominant_power) { + dominant_power = power; + dominant = aarx; + } + } + if (dominant < 0 || dominant_power <= 1e-12f || rx_claimed[dominant]) + return false; + + /* Bound the aggregate ignored energy to -20 dB. PDSCH channel + * estimation leaves small fixed-point residue on otherwise diagonal + * RF-simulator/isolated channels; a -30 dB total bound was brittle and + * needlessly selected the full 8x8 solver. At or above one percent, + * retain the exact joint detector. */ + const float leakage_power = fmaxf(0.0f, total_power - dominant_power); + if (leakage_power > dominant_power * 1e-2f) + return false; + dominant_rx[layer] = dominant; + rx_claimed[dominant] = true; + } + return true; +} + +static void nr_high_rank_equalize_permutation_diagonal( + uint32_t rx_size_symbol, + uint8_t n_rx, + uint8_t nl, + const c16_t rxdataF_ext[][rx_size_symbol], + c16_t rxdataF_comp[][nl][n_rx][rx_size_symbol], + uint8_t symbol, + int first_re, + int group_length, + const float complex h[MAX_ANT][NR_MAX_NB_LAYERS], + const int dominant_rx[NR_MAX_NB_LAYERS]) +{ + float complex coefficient[NR_MAX_NB_LAYERS] = {0}; + for (int layer = 0; layer < nl; layer++) { + const float complex dominant_h = h[dominant_rx[layer]][layer]; + const float power = crealf(dominant_h * conjf(dominant_h)); + coefficient[layer] = 0.7071067811865475244f * conjf(dominant_h) / power; + } + + for (int re = first_re; re < first_re + group_length; re++) { + for (int layer = 0; layer < nl; layer++) { + const c16_t y = rxdataF_ext[dominant_rx[layer]][re]; + const float complex detected = coefficient[layer] * ((float)y.r + I * (float)y.i); + const long real = lroundf(crealf(detected)); + const long imag = lroundf(cimagf(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); + } + } +} + +/* Unitary precoders produce dense effective channel matrices whose columns + * are nevertheless orthogonal. In that case H^H H is diagonal and the MMSE + * solution (after removing its scalar bias) is exactly a normalized matched + * filter; no Cholesky factorization is required. */ +static bool nr_high_rank_orthogonal(uint8_t nl, + const float complex gram[NR_MAX_NB_LAYERS][NR_MAX_NB_LAYERS]) +{ + for (int row = 0; row < nl; row++) { + const float row_power = crealf(gram[row][row]); + if (!isfinite(row_power) || row_power <= 1e-12f) + return false; + float normalized_interference = 0.0f; + for (int col = 0; col < nl; col++) { + if (col == row) + continue; + const float col_power = crealf(gram[col][col]); + if (!isfinite(col_power) || col_power <= 1e-12f) + return false; + const float cross_power = crealf(gram[row][col] * conjf(gram[row][col])); + normalized_interference += cross_power / (row_power * col_power); + } + /* The normalized matched-filter output sees this sum as residual + * inter-layer interference. A one-percent aggregate limit guarantees + * at least 20 dB spatial isolation; less isolated channels use the full + * Cholesky detector. */ + if (!isfinite(normalized_interference) || normalized_interference > 1e-2f) + return false; + } + return true; +} + +static void nr_high_rank_log_path_once(unsigned int path, uint8_t nl, int re_group) +{ + static bool logged[3] = {false}; + if (!__atomic_exchange_n(&logged[path], true, __ATOMIC_RELAXED)) { + static const char *const name[3] = {"permutation-diagonal", "orthogonal", "full-cholesky"}; + LOG_I(NR_PHY, + "[HIGH_RANK_PATH] layers=%u re_group=%d detector=%s\n", + nl, + re_group, + name[path]); + } +} + +static void nr_high_rank_equalize_orthogonal( + uint32_t rx_size_symbol, + uint8_t n_rx, + uint8_t nl, + const c16_t rxdataF_ext[][rx_size_symbol], + c16_t rxdataF_comp[][nl][n_rx][rx_size_symbol], + uint8_t symbol, + int first_re, + int group_length, + const float complex h[MAX_ANT][NR_MAX_NB_LAYERS], + const float complex gram[NR_MAX_NB_LAYERS][NR_MAX_NB_LAYERS]) +{ + float complex coefficient[NR_MAX_NB_LAYERS][MAX_ANT] = {{0}}; + for (int layer = 0; layer < nl; layer++) { + const float normalization = 0.7071067811865475244f / crealf(gram[layer][layer]); + for (int aarx = 0; aarx < n_rx; aarx++) + coefficient[layer][aarx] = normalization * conjf(h[aarx][layer]); + } + + for (int re = first_re; re < first_re + group_length; re++) { + for (int layer = 0; layer < nl; layer++) { + float complex detected = 0.0f; + for (int aarx = 0; aarx < n_rx; aarx++) { + const c16_t y = rxdataF_ext[aarx][re]; + detected += coefficient[layer][aarx] * ((float)y.r + I * (float)y.i); + } + const long real = lroundf(crealf(detected)); + const long imag = lroundf(cimagf(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); + } + } +} + +static int nr_dlsch_mmse_high_rank_cholesky(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, + int re_group) +{ + 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 float inverse_q15 = 1.0f / 32768.0f; + const float normalized_noise = (float)noise_var * inverse_q15 * inverse_q15; + + for (int first_re = 0; first_re < length; first_re += re_group) { + const int group_length = min(re_group, length - first_re); + const int representative_re = first_re + group_length / 2; + float complex h[MAX_ANT][NR_MAX_NB_LAYERS] = {{0}}; + for (int aarx = 0; aarx < n_rx; aarx++) { + for (int layer = 0; layer < nl; layer++) { + const c16_t *ch = (const c16_t *)dl_ch_estimates_ext[layer * n_rx + aarx]; + h[aarx][layer] = (float)ch[representative_re].r * inverse_q15 + + I * ((float)ch[representative_re].i * inverse_q15); + } + } + int dominant_rx[NR_MAX_NB_LAYERS] = {0}; + if (nr_high_rank_permutation_diagonal(n_rx, nl, h, dominant_rx)) { + nr_high_rank_log_path_once(0, nl, re_group); + nr_high_rank_equalize_permutation_diagonal(rx_size_symbol, + n_rx, + nl, + rxdataF_ext, + rxdataF_comp, + symbol, + first_re, + group_length, + h, + dominant_rx); + continue; + } + + /* The common identity/permutation-diagonal path needs no Gram matrix. + * Build H^H H only after that data-driven check, avoiding O(N^3) work + * while preserving the exact fallback for coupled channels. */ + float complex gram[NR_MAX_NB_LAYERS][NR_MAX_NB_LAYERS] = {{0}}; + for (int row = 0; row < nl; row++) + for (int col = 0; col <= row; col++) { + for (int aarx = 0; aarx < n_rx; aarx++) + gram[row][col] += conjf(h[aarx][row]) * h[aarx][col]; + gram[col][row] = conjf(gram[row][col]); + } + if (nr_high_rank_orthogonal(nl, gram)) { + nr_high_rank_log_path_once(1, nl, re_group); + nr_high_rank_equalize_orthogonal(rx_size_symbol, + n_rx, + nl, + rxdataF_ext, + rxdataF_comp, + symbol, + first_re, + group_length, + h, + gram); + continue; + } + + nr_high_rank_log_path_once(2, nl, re_group); + + float trace = 0.0f; + for (int layer = 0; layer < nl; layer++) + trace += crealf(gram[layer][layer]); + const float average_gain = trace / nl; + const float ridge_floor = fmaxf(average_gain * 1e-7f, 1e-12f); + const float ridge = fmaxf(ridge_floor, fminf(normalized_noise, average_gain * 0.25f)); + for (int layer = 0; layer < nl; layer++) + gram[layer][layer] += ridge; + + float complex L[NR_MAX_NB_LAYERS][NR_MAX_NB_LAYERS]; + if (!nr_high_rank_cholesky(nl, gram, L)) { + LOG_W(NR_PHY, + "Rank-%u Cholesky factorization failed at RE %d; using reference detector\n", + nl, + representative_re); + if (nr_dlsch_mmse_high_rank_reference(rx_size_symbol, + n_rx, + nl, + rxdataF_ext, + dl_ch_estimates_ext, + rxdataF_comp, + symbol, + first_re, + group_length, + noise_var) + < 0) + return -1; + continue; + } + + /* For the exact one-RE mode, solve A*x=H^H*y directly. This avoids + * constructing all n_rx columns of W just to multiply them by one y. */ + if (group_length == 1) { + float complex rhs[NR_MAX_NB_LAYERS] = {0}; + float complex detected[NR_MAX_NB_LAYERS] = {0}; + for (int layer = 0; layer < nl; layer++) + for (int aarx = 0; aarx < n_rx; aarx++) { + const c16_t y = rxdataF_ext[aarx][first_re]; + rhs[layer] += conjf(h[aarx][layer]) * ((float)y.r + I * (float)y.i); + } + nr_high_rank_cholesky_solve(nl, L, rhs, detected); + float inverse_diagonal[NR_MAX_NB_LAYERS] = {0}; + nr_high_rank_inverse_diagonal(nl, L, inverse_diagonal); + for (int layer = 0; layer < nl; layer++) { + const float gain = fmaxf(0.05f, 1.0f - ridge * inverse_diagonal[layer]); + detected[layer] *= 0.7071067811865475244f / gain; + const long real = lroundf(crealf(detected[layer])); + const long imag = lroundf(cimagf(detected[layer])); + rxdataF_comp[symbol][layer][0][first_re].r = min(max(real, -32767), 32767); + rxdataF_comp[symbol][layer][0][first_re].i = min(max(imag, -32767), 32767); + } + continue; + } + + /* W=(H^H H + lambda I)^-1 H^H. Reusing W across a configurable + * frequency group is an optional approximation; group=1 is exact. */ + float complex weights[NR_MAX_NB_LAYERS][MAX_ANT] = {{0}}; + for (int aarx = 0; aarx < n_rx; aarx++) { + float complex b[NR_MAX_NB_LAYERS] = {0}; + float complex x[NR_MAX_NB_LAYERS] = {0}; + for (int layer = 0; layer < nl; layer++) + b[layer] = conjf(h[aarx][layer]); + nr_high_rank_cholesky_solve(nl, L, b, x); + for (int layer = 0; layer < nl; layer++) + weights[layer][aarx] = x[layer]; + } + + float gain[NR_MAX_NB_LAYERS] = {0}; + for (int layer = 0; layer < nl; layer++) { + float complex diagonal = 0.0f; + for (int aarx = 0; aarx < n_rx; aarx++) + diagonal += weights[layer][aarx] * h[aarx][layer]; + gain[layer] = fmaxf(0.05f, crealf(diagonal)); + } + + for (int re = first_re; re < first_re + group_length; re++) { + for (int layer = 0; layer < nl; layer++) { + float complex detected = 0.0f; + for (int aarx = 0; aarx < n_rx; aarx++) { + const c16_t y = rxdataF_ext[aarx][re]; + detected += weights[layer][aarx] * ((float)y.r + I * (float)y.i); + } + detected *= 0.7071067811865475244f / gain[layer]; + const long real = lroundf(crealf(detected)); + const long imag = lroundf(cimagf(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 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) +{ + const nr_high_rank_detector_config_t *config = get_high_rank_detector_config(); + if (config->detector == NR_HIGH_RANK_REFERENCE) + return nr_dlsch_mmse_high_rank_reference(rx_size_symbol, + n_rx, + nl, + rxdataF_ext, + dl_ch_estimates_ext, + rxdataF_comp, + symbol, + 0, + length, + noise_var); + return nr_dlsch_mmse_high_rank_cholesky(rx_size_symbol, + n_rx, + nl, + rxdataF_ext, + dl_ch_estimates_ext, + rxdataF_comp, + symbol, + length, + noise_var, + config->re_group); +} + static void set_high_rank_qam_thresholds(uint8_t mod_order, int length, c16_t *mag, diff --git a/openair1/SCHED_NR_UE/phy_procedures_nr_ue.c b/openair1/SCHED_NR_UE/phy_procedures_nr_ue.c index 18b02cf0ff..b1a9759d09 100644 --- a/openair1/SCHED_NR_UE/phy_procedures_nr_ue.c +++ b/openair1/SCHED_NR_UE/phy_procedures_nr_ue.c @@ -55,6 +55,44 @@ static const unsigned int gain_table[31] = {100, 112, 126, 141, 158, 178, 359, 398, 447, 501, 562, 631, 708, 794, 891, 1000, 1122, 1258, 1412, 1585, 1778, 1995, 2239, 2512, 2818, 3162}; +typedef struct { + fourDimArray_t *array; + size_t capacity; +} nr_ue_reusable_array_t; + +/* Return a correctly-strided view without shrinking an existing allocation. + * Rank and RB allocation vary from one PDSCH to the next; exact-dimension + * resize logic otherwise oscillates between small control grants and large + * rank-8 data grants, reintroducing allocation/zeroing on every slot. */ +static fourDimArray_t *nr_ue_reuse_array(nr_ue_reusable_array_t *storage, + size_t element_size, + int dim1, + int dim2, + int dim3, + int dim4) +{ + DevAssert(dim1 > 0); + size_t elements = dim1; + if (dim2 > 0) + elements *= dim2; + if (dim3 > 0) + elements *= dim3; + if (dim4 > 0) + elements *= dim4; + const size_t required = sizeof(fourDimArray_t) + elements * element_size; + if (required > storage->capacity) { + free(storage->array); + storage->array = malloc16_clear(required); + AssertFatal(storage->array != NULL, "Could not allocate %zu-byte UE PDSCH workspace\n", required); + storage->capacity = required; + } + storage->array->dim1 = dim1; + storage->array->dim2 = dim2; + storage->array->dim3 = dim3; + storage->array->dim4 = dim4; + return storage->array; +} + static void nr_ue_prach_procedures(PHY_VARS_NR_UE *ue, const UE_nr_rxtx_proc_t *proc, c16_t **txData); void nr_fill_dl_indication(nr_downlink_indication_t *dl_ind, @@ -517,6 +555,18 @@ static int nr_ue_pdsch_procedures(PHY_VARS_NR_UE *ue, int harq_pid = dlschCfg->harq_process_nbr; + /* These workspaces are several tens of megabytes at rank 8. Allocating and + * zeroing them for every PDSCH made the UE miss its HARQ-feedback slot even + * when the cheap permutation-diagonal detector was selected. Each DL actor + * owns one persistent thread, so thread-local storage is race-free and + * preserves the legacy zero-initialized first use. Every active RE is + * overwritten by channel estimation/equalization before it is consumed. */ + static _Thread_local nr_ue_reusable_array_t pdsch_dl_ch_estimates_storage; + static _Thread_local nr_ue_reusable_array_t rxdataF_comp_storage; + static _Thread_local nr_ue_reusable_array_t dl_ch_mag_storage; + static _Thread_local nr_ue_reusable_array_t dl_ch_magb_storage; + static _Thread_local nr_ue_reusable_array_t dl_ch_magr_storage; + LOG_D(PHY, "[UE %d] frame_rx %d, nr_slot_rx %d, harq_pid %d (%d), BWP start %d, start RB %d, end RB %d, symbol_start %d, nb_symbols " "%d, DMRS mask " @@ -535,8 +585,14 @@ static int nr_ue_pdsch_procedures(PHY_VARS_NR_UE *ue, dlsch->cw_info.Nl); const uint32_t pdsch_est_size = ((ue->frame_parms.symbols_per_slot * ue->frame_parms.ofdm_symbol_size + 15) / 16) * 16; - fourDimArray_t *toFree = NULL; - allocCast2D(pdsch_dl_ch_estimates, int32_t, toFree, ue->frame_parms.nb_antennas_rx * dlsch->cw_info.Nl, pdsch_est_size, false); + fourDimArray_t *pdsch_dl_ch_estimates_view = + nr_ue_reuse_array(&pdsch_dl_ch_estimates_storage, + sizeof(int32_t), + ue->frame_parms.nb_antennas_rx * dlsch->cw_info.Nl, + pdsch_est_size, + 0, + 0); + cast2Darray(pdsch_dl_ch_estimates, int32_t, pdsch_dl_ch_estimates_view); c16_t ptrs_phase_per_slot[ue->frame_parms.nb_antennas_rx][NR_SYMBOLS_PER_SLOT]; memset(ptrs_phase_per_slot, 0, sizeof(ptrs_phase_per_slot)); @@ -545,15 +601,13 @@ static int nr_ue_pdsch_procedures(PHY_VARS_NR_UE *ue, memset(ptrs_re_per_slot, 0, sizeof(ptrs_re_per_slot)); const uint32_t rx_size_symbol = (freq_alloc->num_rbs * NR_NB_SC_PER_RB + 15) & ~15; - fourDimArray_t *toFree2 = NULL; - allocCast4D(rxdataF_comp, - c16_t, - toFree2, - ue->frame_parms.symbols_per_slot, - dlsch->cw_info.Nl, - ue->frame_parms.nb_antennas_rx, - rx_size_symbol, - false); + fourDimArray_t *rxdataF_comp_view = nr_ue_reuse_array(&rxdataF_comp_storage, + sizeof(c16_t), + ue->frame_parms.symbols_per_slot, + dlsch->cw_info.Nl, + ue->frame_parms.nb_antennas_rx, + rx_size_symbol); + cast4Darray(rxdataF_comp, c16_t, rxdataF_comp_view); uint32_t nvar = 0; @@ -644,33 +698,27 @@ static int nr_ue_pdsch_procedures(PHY_VARS_NR_UE *ue, freq_alloc->num_rbs * NR_NB_SC_PER_RB * dlschCfg->number_symbols, &mt); } - fourDimArray_t *toFree3 = NULL; - allocCast4D(dl_ch_mag, - c16_t, - toFree3, - NR_SYMBOLS_PER_SLOT, - dlsch->cw_info.Nl, - ue->frame_parms.nb_antennas_rx, - rx_size_symbol, - false); - fourDimArray_t *toFree4 = NULL; - allocCast4D(dl_ch_magb, - c16_t, - toFree4, - NR_SYMBOLS_PER_SLOT, - dlsch->cw_info.Nl, - ue->frame_parms.nb_antennas_rx, - rx_size_symbol, - false); - fourDimArray_t *toFree5 = NULL; - allocCast4D(dl_ch_magr, - c16_t, - toFree5, - NR_SYMBOLS_PER_SLOT, - dlsch->cw_info.Nl, - ue->frame_parms.nb_antennas_rx, - rx_size_symbol, - false); + fourDimArray_t *dl_ch_mag_view = nr_ue_reuse_array(&dl_ch_mag_storage, + sizeof(c16_t), + NR_SYMBOLS_PER_SLOT, + dlsch->cw_info.Nl, + ue->frame_parms.nb_antennas_rx, + rx_size_symbol); + cast4Darray(dl_ch_mag, c16_t, dl_ch_mag_view); + fourDimArray_t *dl_ch_magb_view = nr_ue_reuse_array(&dl_ch_magb_storage, + sizeof(c16_t), + NR_SYMBOLS_PER_SLOT, + dlsch->cw_info.Nl, + ue->frame_parms.nb_antennas_rx, + rx_size_symbol); + cast4Darray(dl_ch_magb, c16_t, dl_ch_magb_view); + fourDimArray_t *dl_ch_magr_view = nr_ue_reuse_array(&dl_ch_magr_storage, + sizeof(c16_t), + NR_SYMBOLS_PER_SLOT, + dlsch->cw_info.Nl, + ue->frame_parms.nb_antennas_rx, + rx_size_symbol); + cast4Darray(dl_ch_magr, c16_t, dl_ch_magr_view); int pdsch_status = 0; for (int m = dlschCfg->start_symbol; m < (dlschCfg->number_symbols + dlschCfg->start_symbol); m++) { bool first_symbol_flag = false; @@ -717,11 +765,6 @@ static int nr_ue_pdsch_procedures(PHY_VARS_NR_UE *ue, if (scope_req.copy_rxdataF_to_scope) { UEunlockScopeData(ue, pdschRxdataF); } - free(toFree); - free(toFree2); - free(toFree3); - free(toFree4); - free(toFree5); return pdsch_status; } @@ -798,16 +841,11 @@ static void nr_ue_dlsch_procedures(PHY_VARS_NR_UE *ue, LOG_D(PHY, "AbsSubframe %d.%d Start LDPC Decoder for CW%d [harq_pid %d]\n", frame_rx % 1024, nr_slot_rx, cw_idx, harq_pid); - NR_DL_FRAME_PARMS *fp = &ue->frame_parms; // exit dlsch procedures as there are no active dlsch NR_DL_UE_HARQ_t *dl_harq = &ue->dl_harq_processes[cw_idx][harq_pid]; if (dl_harq->status != NR_ACTIVE) { - // don't wait anymore + // The slot-level caller releases the RX/TX barrier after all codewords. LOG_E(NR_PHY, "Internal error nr_ue_dlsch_procedure() called but no active cw on slot %d, harq %d\n", nr_slot_rx, harq_pid); - if (config->k1_feedback) { - const int ack_nack_slot_and_frame = (proc->nr_slot_rx + config->k1_feedback) + proc->frame_rx * fp->slots_per_frame; - dynamic_barrier_join(&ue->process_slot_tx_barriers[ack_nack_slot_and_frame % NUM_PROCESS_SLOT_TX_BARRIERS]); - } return; } @@ -855,12 +893,6 @@ static void nr_ue_dlsch_procedures(PHY_VARS_NR_UE *ue, ue->if_inst->dl_indication(&dl_indication); } - // DLSCH decoding finished! don't wait anymore in Tx process, we know if we should answer ACK/NACK PUCCH - if ((dlsch->rnti_type == TYPE_C_RNTI_ || dlsch->rnti_type == TYPE_RA_RNTI_) && config->k1_feedback) { - const int ack_nack_slot_and_frame = (proc->nr_slot_rx + config->k1_feedback) + proc->frame_rx * fp->slots_per_frame; - dynamic_barrier_join(&ue->process_slot_tx_barriers[ack_nack_slot_and_frame % NUM_PROCESS_SLOT_TX_BARRIERS]); - } - int a_segments = MAX_NUM_NR_DLSCH_SEGMENTS_PER_LAYER * NR_MAX_NB_LAYERS; // number of segments to be allocated if (freq_alloc->num_rbs != 273) { a_segments = a_segments * freq_alloc->num_rbs; @@ -1218,6 +1250,8 @@ void pdsch_processing(PHY_VARS_NR_UE *ue, const UE_nr_rxtx_proc_t *proc, nr_phy_ unav_res += compute_csi_rm_unav_res(dlsch_config); int G[NR_MAX_NB_CODEWORDS] = {0}; uint32_t rx_llr_buf_sz[NR_MAX_NB_CODEWORDS] = {0}; + static _Thread_local nr_ue_reusable_array_t llr_storage[NR_MAX_NB_CODEWORDS]; + static _Thread_local nr_ue_reusable_array_t joint_llr_storage; 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, @@ -1228,7 +1262,9 @@ void pdsch_processing(PHY_VARS_NR_UE *ue, const UE_nr_rxtx_proc_t *proc, nr_phy_ 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)); + fourDimArray_t *llr_view = + nr_ue_reuse_array(&llr_storage[c], sizeof(int16_t), rx_llr_buf_sz[c], 0, 0, 0); + llr[c] = (int16_t *)llr_view->data; } int ret_pdsch = 0; @@ -1256,7 +1292,9 @@ void pdsch_processing(PHY_VARS_NR_UE *ue, const UE_nr_rxtx_proc_t *proc, nr_phy_ 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)); + fourDimArray_t *joint_llr_view = + nr_ue_reuse_array(&joint_llr_storage, sizeof(int16_t), joint_buf_sz, 0, 0, 0); + int16_t *joint_llr = (int16_t *)joint_llr_view->data; 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( @@ -1286,7 +1324,6 @@ void pdsch_processing(PHY_VARS_NR_UE *ue, const UE_nr_rxtx_proc_t *proc, nr_phy_ G[0], G[1]); } - free(joint_llr); } } else { NR_UE_DLSCH_t *dlsch = &phy_data->dlsch[0]; @@ -1307,11 +1344,6 @@ void pdsch_processing(PHY_VARS_NR_UE *ue, const UE_nr_rxtx_proc_t *proc, nr_phy_ 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); @@ -1322,6 +1354,23 @@ void pdsch_processing(PHY_VARS_NR_UE *ue, const UE_nr_rxtx_proc_t *proc, nr_phy_ ue->phy_cpu_stats.cpu_time_stats[DLSCH_PROCEDURES_STATS].p_time / (cpuf * 1000.0)); } + /* One PDSCH occasion has one RX/TX synchronization point even when it + * carries two transport blocks. Joining this barrier in each codeword's + * decoder advances it twice: the second join waits for a TX participant + * in the next generation and deadlocks the UE after its first rank>4 + * PDSCH. Publish both ACK/NACK results first, then release TX once. */ + bool needs_ack_barrier = false; + for (int c = 0; c < phy_data->n_dlsch_codewords; c++) { + const NR_UE_DLSCH_t *dlsch = &phy_data->dlsch[c]; + needs_ack_barrier |= dlsch->active + && (dlsch->rnti_type == TYPE_C_RNTI_ || dlsch->rnti_type == TYPE_RA_RNTI_); + } + if (needs_ack_barrier && 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]); + } + 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], @@ -1335,8 +1384,6 @@ void pdsch_processing(PHY_VARS_NR_UE *ue, const UE_nr_rxtx_proc_t *proc, nr_phy_ } } - 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/dlsim.c b/openair1/SIMULATION/NR_PHY/dlsim.c index 13134a4102..cb696a81ce 100644 --- a/openair1/SIMULATION/NR_PHY/dlsim.c +++ b/openair1/SIMULATION/NR_PHY/dlsim.c @@ -745,6 +745,7 @@ int main(int argc, char **argv) .num_dlharq = 16, .num_ulharq = 16, .maxMIMO_layers = g_nrOfLayers, + .pdsch_dmrs_additional_position = 1, .force_256qam_off = false, .timer_config.sr_ProhibitTimer = 0, .timer_config.sr_TransMax = 64, @@ -1076,6 +1077,10 @@ int main(int argc, char **argv) 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); + /* The high-rank PDSCH carries two independently HARQ-managed TBs. + * Advance both NDIs between independent trials; leaving CW1's NDI + * unchanged makes the UE treat every later trial as a retransmission. */ + UE_info->UE_sched_ctrl.harq_processes[harq_pid].ndi2 = !(trial&1); UE_info->UE_sched_ctrl.harq_processes[harq_pid].round = round; // nr_schedule_ue_spec() requires the mutex to be locked @@ -1702,6 +1707,13 @@ void update_dmrs_config(NR_CellGroupConfig_t *scg, int8_t* dmrs_arg) NR_DMRS_DownlinkConfig_t *dmrs_config = (mapping_type == typeA) ? dmrs_MappingtypeA->choice.setup : dmrs_MappingtypeB->choice.setup; + /* Ranks 5--8 need the two-symbol Type-1 DMRS that provides ports 1000--1007. + * Keep that mandatory maxLength setting when -U changes only the additional + * DMRS position; otherwise the high-rank scheduler correctly rejects the + * inconsistent single-symbol configuration. */ + if (g_nrOfLayers > 4) + asn1cCallocOne(dmrs_config->maxLength, NR_DMRS_DownlinkConfig__maxLength_len2); + if (add_pos != 2) { // pos0,pos1,pos3 if (dmrs_config->dmrs_AdditionalPosition == NULL) { dmrs_config->dmrs_AdditionalPosition = calloc(1,sizeof(*dmrs_MappingtypeA->choice.setup->dmrs_AdditionalPosition)); diff --git a/openair2/E2AP/RAN_FUNCTION/CUSTOMIZED/ran_func_mac.c b/openair2/E2AP/RAN_FUNCTION/CUSTOMIZED/ran_func_mac.c index 2a8d8526fa..88a6305b5f 100644 --- a/openair2/E2AP/RAN_FUNCTION/CUSTOMIZED/ran_func_mac.c +++ b/openair2/E2AP/RAN_FUNCTION/CUSTOMIZED/ran_func_mac.c @@ -17,6 +17,11 @@ bool read_mac_sm(void* data) //fill_mac_ind_data(mac); mac->msg.tstamp = time_now_us(); + // Export the physical denominator beside the per-UE TBS/PRB counters. This + // avoids estimating PRB opportunity from a rounded, differently-windowed + // KPM percentage in the rApp. + mac->msg.dl_prb_opportunities = RC.nrmac[mod_id]->mac_stats.dl.total_prb_aggregate; + mac->msg.dl_used_prbs = RC.nrmac[mod_id]->mac_stats.dl.used_prb_aggregate; NR_UEs_t *UE_info = &RC.nrmac[mod_id]->UE_info; size_t num_ues = 0; @@ -33,6 +38,10 @@ bool read_mac_sm(void* data) size_t i = 0; //TODO UE_iterator(UE_info->connected_ue_list, UE) { + // Bound the write against the calloc'd buffer size: a UE attach between + // the count loop above and this iterator can grow connected_ue_list and + // overflow mac->msg.ue_stats (corrupts the heap → later free() abort). + if (i >= num_ues) break; const NR_UE_sched_ctrl_t* sched_ctrl = &UE->UE_sched_ctrl; mac_ue_stats_impl_t* rd = &mac->msg.ue_stats[i]; @@ -58,8 +67,11 @@ bool read_mac_sm(void* data) rd->dl_aggr_retx_prb = UE->mac_stats.dl.total_rbs_retx; rd->ul_aggr_retx_prb = UE->mac_stats.ul.total_rbs_retx; - rd->dl_aggr_bytes_sdus = UE->mac_stats.dl.lc_bytes[3]; - rd->ul_aggr_bytes_sdus = UE->mac_stats.ul.lc_bytes[3]; + // The service-model field is aggregate SDU bytes. Using LCID 3 silently + // drops payload whenever the DRB is assigned another logical channel and + // corrupts the MAC payload/TBS fill ratio used by capacity estimation. + rd->dl_aggr_bytes_sdus = UE->mac_stats.dl.total_sdu_bytes; + rd->ul_aggr_bytes_sdus = UE->mac_stats.ul.total_sdu_bytes; rd->dl_aggr_sdus = UE->mac_stats.dl.num_mac_sdu; rd->ul_aggr_sdus = UE->mac_stats.ul.num_mac_sdu; @@ -68,6 +80,16 @@ bool read_mac_sm(void* data) rd->pucch_snr = sched_ctrl->pucch_pc.avg_snr; //: float = -64; rd->wb_cqi = sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.wb_cqi_1tb; + rd->csi_valid = sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.print_report; + rd->dl_ri = rd->csi_valid ? sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.ri + 1 : 0; + rd->dl_layers = UE->mac_stats.dl.last_layers; + rd->pmi_x1 = sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.pmi_x1; + rd->pmi_x2 = sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.pmi_x2; + rd->csi_frame = sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.report_frame; + rd->csi_slot = sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.report_slot; + rd->active_tx_chains = RC.nrmac[mod_id]->antenna_state != NULL + ? nr_get_active_tx_antenna_count(RC.nrmac[mod_id]->antenna_state) + : 0; rd->dl_mcs1 = sched_ctrl->dl_bler_stats.mcs; rd->dl_bler = sched_ctrl->dl_bler_stats.bler; rd->ul_mcs1 = sched_ctrl->ul_bler_stats.mcs; @@ -110,4 +132,3 @@ sm_ag_if_ans_t write_ctrl_mac_sm(void const* data) sm_ag_if_ans_t ans = {0}; return ans; } - diff --git a/openair2/E2AP/RAN_FUNCTION/CUSTOMIZED/ran_func_pdcp.c b/openair2/E2AP/RAN_FUNCTION/CUSTOMIZED/ran_func_pdcp.c index 6bb956e9c5..07d6224f9b 100644 --- a/openair2/E2AP/RAN_FUNCTION/CUSTOMIZED/ran_func_pdcp.c +++ b/openair2/E2AP/RAN_FUNCTION/CUSTOMIZED/ran_func_pdcp.c @@ -25,7 +25,7 @@ #endif #if defined (NGRAN_GNB_CUUP) -void fill_pdcp_stats_for_all_rbs(pdcp_radio_bearer_stats_t* rb, const size_t num_ues, const uint64_t* ue_id_list) +void fill_pdcp_stats_for_all_rbs(pdcp_radio_bearer_stats_t* rb, const size_t num_ues, const uint64_t* ue_id_list, const size_t rb_capacity) { size_t j = 0; for (size_t i = 0; i < num_ues; i++) { @@ -37,6 +37,10 @@ void fill_pdcp_stats_for_all_rbs(pdcp_radio_bearer_stats_t* rb, const size_t num if (!rc) continue; + // Bound the write against the original capacity: an RB activating + // between the count loop in read_pdcp_sm and this fill can grow the + // observed count and overflow rb[] (corrupts the heap). + if (j >= rb_capacity) return; pdcp_radio_bearer_stats_t *rd = &rb[j++]; rd->txpdu_pkts = rb_pdcp.txpdu_pkts; /* aggregated number of tx packets */ @@ -114,7 +118,7 @@ bool read_pdcp_sm(void* data) // Filling Radio Bearer Statistics #if defined (NGRAN_GNB_CUUP) - fill_pdcp_stats_for_all_rbs(pdcp->msg.rb, num_ues, ue_id_list); + fill_pdcp_stats_for_all_rbs(pdcp->msg.rb, num_ues, ue_id_list, pdcp->msg.len); #endif return true; } diff --git a/openair2/E2AP/RAN_FUNCTION/CUSTOMIZED/ran_func_rlc.c b/openair2/E2AP/RAN_FUNCTION/CUSTOMIZED/ran_func_rlc.c index 5521adf9ae..9f849a8ad1 100644 --- a/openair2/E2AP/RAN_FUNCTION/CUSTOMIZED/ran_func_rlc.c +++ b/openair2/E2AP/RAN_FUNCTION/CUSTOMIZED/ran_func_rlc.c @@ -83,6 +83,11 @@ bool read_rlc_sm(void* data) const int srb_flag = 0; const bool rc = nr_rlc_get_statistics(rnti, srb_flag, rb_id, &rb_rlc); if(!rc) continue; + // Bound the write against the originally-calloc'd buffer size. A UE + // attach between num_act_rb() above and this iterator can grow the + // connected-UE list; without this check we'd overflow rlc->msg.rb + // and corrupt the heap (crash later in free_rlc_ind_msg()). + if (i >= act_rb) goto done_iter; rlc_radio_bearer_stats_t* sm_rb = &rlc->msg.rb[i]; /* TX */ @@ -143,6 +148,7 @@ bool read_rlc_sm(void* data) ++i; } } +done_iter: return act_rb > 0; } diff --git a/openair2/E2AP/RAN_FUNCTION/O-RAN/ran_func_kpm_subs.c b/openair2/E2AP/RAN_FUNCTION/O-RAN/ran_func_kpm_subs.c index e4ffbbdaed..89534bba46 100644 --- a/openair2/E2AP/RAN_FUNCTION/O-RAN/ran_func_kpm_subs.c +++ b/openair2/E2AP/RAN_FUNCTION/O-RAN/ran_func_kpm_subs.c @@ -86,8 +86,42 @@ static meas_record_lst_t fill_DRB_PdcpSduVolumeUL(__attribute__((unused)) const } #if defined (NGRAN_GNB_DU) -static uldlcounter_t last_rlc_pdu_total_bytes[MAX_MOBILES_PER_GNB] = {0}; -static uldlcounter_t last_total_prbs[MAX_MOBILES_PER_GNB] = {0}; +typedef struct ue_uldlcounter { + const NR_UE_info_t *ue; + rnti_t rnti; + uldlcounter_t value; + bool dl_valid; + bool ul_valid; +} ue_uldlcounter_t; + +static ue_uldlcounter_t last_rlc_pdu_total_bytes[MAX_MOBILES_PER_GNB] = {0}; +static ue_uldlcounter_t last_total_prbs[MAX_MOBILES_PER_GNB] = {0}; + +/* The connected UE list is compacted when a UE leaves, so its iteration index + * is not a stable identity. Keep baselines by the MAC-assigned UID instead and + * reset them when that UID is reused for another UE. */ +static ue_uldlcounter_t *get_ue_counter(ue_uldlcounter_t counters[MAX_MOBILES_PER_GNB], + const cudu_ue_info_pair_t ue_info) +{ + AssertFatal(ue_info.ue != NULL, "KPM UE measurement requires MAC UE information\n"); + const uid_t uid = ue_info.ue->uid; + AssertFatal(uid < MAX_MOBILES_PER_GNB, "KPM UE UID %u exceeds maximum %u\n", uid, MAX_MOBILES_PER_GNB); + + ue_uldlcounter_t *counter = &counters[uid]; + if (counter->ue != ue_info.ue || counter->rnti != ue_info.ue->rnti) + *counter = (ue_uldlcounter_t){.ue = ue_info.ue, .rnti = ue_info.ue->rnti}; + return counter; +} + +/* A new/reused UE counter and a reset/wrap have no valid preceding sample. + * Establish a fresh baseline and report zero for that single period. */ +static uint32_t counter_delta(uint32_t current, uint32_t *previous, bool *valid) +{ + const uint32_t delta = *valid && current >= *previous ? current - *previous : 0; + *previous = current; + *valid = true; + return delta; +} static nr_rlc_statistics_t get_rlc_stats_per_drb(const rnti_t rnti, const int rb_id) { @@ -120,8 +154,16 @@ static meas_record_lst_t fill_DRB_RlcSduDelayDl(__attribute__((unused)) const la meas_record.value = REAL_MEAS_VALUE; - // Get the value of sojourn time at the RLC buffer - meas_record.real_val = rlc.txsdu_avg_time_to_tx; // [μs] + // Report the windowed-average RLC SDU sojourn (mean per-packet wait over the + // last 100 ms), not the instantaneous tx_list HOL snapshot. The snapshot + // (txsdu_wt_us) reads 0 whenever the tx_list is momentarily drained at the + // single sampling instant -- common with bursty DL even at high throughput -- + // which systematically under-reports delay (era/load-dependent). The averaging + // accumulator is now activated once (reset guarded to off->on in + // nr_rlc_activate_avg_time_to_tx), so it accumulates continuously instead of + // being wiped on every read; it returns 0 only when no SDU was transmitted in + // the window (genuine idle/starvation). + meas_record.real_val = (double)rlc.txsdu_avg_time_to_tx; // [us] avg RLC SDU sojourn return meas_record; } @@ -132,7 +174,7 @@ static meas_record_lst_t fill_DRB_RlcSduDelayDl(__attribute__((unused)) const la static meas_record_lst_t fill_DRB_UEThpDl(__attribute__((unused)) const label_info_lst_t label, uint32_t gran_period_ms, cudu_ue_info_pair_t ue_info, - const size_t ue_idx, + __attribute__((unused)) const size_t ue_idx, __attribute__((unused))e2_node_level_stats_t* node_stats) { meas_record_lst_t meas_record = {0}; @@ -142,8 +184,9 @@ static meas_record_lst_t fill_DRB_UEThpDl(__attribute__((unused)) const label_in meas_record.value = REAL_MEAS_VALUE; // Calculate DL Thp - meas_record.real_val = (double)(rlc.txpdu_bytes - last_rlc_pdu_total_bytes[ue_idx].dl)*8/gran_period_ms; // [kbps] - last_rlc_pdu_total_bytes[ue_idx].dl = rlc.txpdu_bytes; + ue_uldlcounter_t *counter = get_ue_counter(last_rlc_pdu_total_bytes, ue_info); + const uint32_t bytes = counter_delta(rlc.txpdu_bytes, &counter->value.dl, &counter->dl_valid); + meas_record.real_val = gran_period_ms == 0 ? 0 : (double)bytes * 8 / gran_period_ms; // [kbps] return meas_record; } @@ -154,7 +197,7 @@ static meas_record_lst_t fill_DRB_UEThpDl(__attribute__((unused)) const label_in static meas_record_lst_t fill_DRB_UEThpUl(__attribute__((unused)) const label_info_lst_t label, uint32_t gran_period_ms, cudu_ue_info_pair_t ue_info, - const size_t ue_idx, + __attribute__((unused)) const size_t ue_idx, __attribute__((unused))e2_node_level_stats_t* node_stats) { meas_record_lst_t meas_record = {0}; @@ -166,8 +209,9 @@ static meas_record_lst_t fill_DRB_UEThpUl(__attribute__((unused)) const label_in meas_record.value = REAL_MEAS_VALUE; // Calculate UL Thp - meas_record.real_val = (double)(rlc.rxpdu_bytes - last_rlc_pdu_total_bytes[ue_idx].ul)*8/gran_period_ms; // [kbps] - last_rlc_pdu_total_bytes[ue_idx].ul = rlc.rxpdu_bytes; + ue_uldlcounter_t *counter = get_ue_counter(last_rlc_pdu_total_bytes, ue_info); + const uint32_t bytes = counter_delta(rlc.rxpdu_bytes, &counter->value.ul, &counter->ul_valid); + meas_record.real_val = gran_period_ms == 0 ? 0 : (double)bytes * 8 / gran_period_ms; // [kbps] return meas_record; } @@ -177,7 +221,7 @@ static meas_record_lst_t fill_DRB_UEThpUl(__attribute__((unused)) const label_in static meas_record_lst_t fill_RRU_PrbTotDl(__attribute__((unused)) const label_info_lst_t label, __attribute__((unused)) uint32_t gran_period_ms, cudu_ue_info_pair_t ue_info, - const size_t ue_idx, + __attribute__((unused)) const size_t ue_idx, e2_node_level_stats_t* node_stats) { meas_record_lst_t meas_record = {0}; @@ -185,8 +229,13 @@ static meas_record_lst_t fill_RRU_PrbTotDl(__attribute__((unused)) const label_i meas_record.value = INTEGER_MEAS_VALUE; // Get the number of DL PRBs - meas_record.int_val = (ue_info.ue->mac_stats.dl.total_rbs - last_total_prbs[ue_idx].dl) * 100 / (node_stats[1].mac_stats.dl.total_prb_aggregate - node_stats[0].mac_stats.dl.total_prb_aggregate); // [%] - last_total_prbs[ue_idx].dl = ue_info.ue->mac_stats.dl.total_rbs; + ue_uldlcounter_t *counter = get_ue_counter(last_total_prbs, ue_info); + const uint32_t ue_delta = counter_delta(ue_info.ue->mac_stats.dl.total_rbs, &counter->value.dl, &counter->dl_valid); + const uint64_t current = node_stats[1].mac_stats.dl.total_prb_aggregate; + const uint64_t previous = node_stats[0].mac_stats.dl.total_prb_aggregate; + const uint64_t total_delta = current >= previous ? current - previous : 0; + const uint64_t percentage = total_delta == 0 ? 0 : (uint64_t)ue_delta * 100 / total_delta; + meas_record.int_val = percentage > 100 ? 100 : percentage; // [%] return meas_record; } @@ -196,7 +245,7 @@ static meas_record_lst_t fill_RRU_PrbTotDl(__attribute__((unused)) const label_i static meas_record_lst_t fill_RRU_PrbTotUl(__attribute__((unused)) const label_info_lst_t label, __attribute__((unused)) uint32_t gran_period_ms, cudu_ue_info_pair_t ue_info, - const size_t ue_idx, + __attribute__((unused)) const size_t ue_idx, e2_node_level_stats_t* node_stats) { meas_record_lst_t meas_record = {0}; @@ -204,8 +253,13 @@ static meas_record_lst_t fill_RRU_PrbTotUl(__attribute__((unused)) const label_i meas_record.value = INTEGER_MEAS_VALUE; // Get the number of UL PRBs - meas_record.int_val = (ue_info.ue->mac_stats.ul.total_rbs - last_total_prbs[ue_idx].ul) * 100 / (node_stats[1].mac_stats.ul.total_prb_aggregate - node_stats[0].mac_stats.ul.total_prb_aggregate); // [%] - last_total_prbs[ue_idx].ul = ue_info.ue->mac_stats.ul.total_rbs; + ue_uldlcounter_t *counter = get_ue_counter(last_total_prbs, ue_info); + const uint32_t ue_delta = counter_delta(ue_info.ue->mac_stats.ul.total_rbs, &counter->value.ul, &counter->ul_valid); + const uint64_t current = node_stats[1].mac_stats.ul.total_prb_aggregate; + const uint64_t previous = node_stats[0].mac_stats.ul.total_prb_aggregate; + const uint64_t total_delta = current >= previous ? current - previous : 0; + const uint64_t percentage = total_delta == 0 ? 0 : (uint64_t)ue_delta * 100 / total_delta; + meas_record.int_val = percentage > 100 ? 100 : percentage; // [%] return meas_record; } diff --git a/openair2/E2AP/flexric b/openair2/E2AP/flexric index 340c36bc83..25353bb973 160000 --- a/openair2/E2AP/flexric +++ b/openair2/E2AP/flexric @@ -1 +1 @@ -Subproject commit 340c36bc8385dc0b9f5d8b2d51d16ff288acee79 +Subproject commit 25353bb97333ed415469bfeea8e34d03c6daa208 diff --git a/openair2/GNB_APP/gnb_config.c b/openair2/GNB_APP/gnb_config.c index 8d052bdba7..e5f217cb67 100644 --- a/openair2/GNB_APP/gnb_config.c +++ b/openair2/GNB_APP/gnb_config.c @@ -645,6 +645,9 @@ void RCconfig_verify(configmodule_interface_t *cfg, ngran_node_t node_type) verify_gnb_param_notset(gnbp, GNB_DO_SRS_IDX, GNB_CONFIG_STRING_DOSRS); verify_gnb_param_notset(gnbp, GNB_FORCE256QAMOFF_IDX, GNB_CONFIG_STRING_FORCE256QAMOFF); verify_gnb_param_notset(gnbp, GNB_MAXMIMOLAYERS_IDX, GNB_CONFIG_STRING_MAXMIMOLAYERS); + verify_gnb_param_notset(gnbp, + GNB_PDSCH_DMRS_ADDITIONAL_POSITION_IDX, + GNB_CONFIG_STRING_PDSCH_DMRS_ADDITIONAL_POSITION); verify_gnb_param_notset(gnbp, GNB_DISABLE_HARQ_IDX, GNB_CONFIG_STRING_DISABLE_HARQ); verify_gnb_param_notset(gnbp, GNB_NUM_DL_HARQ_IDX, GNB_CONFIG_STRING_NUM_DL_HARQPROCESSES); verify_gnb_param_notset(gnbp, GNB_NUM_UL_HARQ_IDX, GNB_CONFIG_STRING_NUM_UL_HARQPROCESSES); @@ -1553,6 +1556,12 @@ void RCconfig_nr_macrlc(configmodule_interface_t *cfg) config.force_UL256qam_off = *GNBParamList.paramarray[0][GNB_FORCEUL256QAMOFF_IDX].iptr; config.use_deltaMCS = *GNBParamList.paramarray[0][GNB_USE_DELTA_MCS_IDX].iptr != 0; config.maxMIMO_layers = *GNBParamList.paramarray[0][GNB_MAXMIMOLAYERS_IDX].iptr; + config.pdsch_dmrs_additional_position = + *GNBParamList.paramarray[0][GNB_PDSCH_DMRS_ADDITIONAL_POSITION_IDX].iptr; + AssertFatal(config.pdsch_dmrs_additional_position == 0 || config.pdsch_dmrs_additional_position == 1, + "%s must be 0 (pos0) or 1 (pos1), got %d\n", + GNB_CONFIG_STRING_PDSCH_DMRS_ADDITIONAL_POSITION, + config.pdsch_dmrs_additional_position); config.disable_harq = *GNBParamList.paramarray[0][GNB_DISABLE_HARQ_IDX].iptr; config.num_dlharq = *GNBParamList.paramarray[0][GNB_NUM_DL_HARQ_IDX].iptr; config.num_ulharq = *GNBParamList.paramarray[0][GNB_NUM_UL_HARQ_IDX].iptr; @@ -1572,6 +1581,9 @@ void RCconfig_nr_macrlc(configmodule_interface_t *cfg) config.num_ulharq); int tot_ant = config.pdsch_AntennaPorts.N1 * config.pdsch_AntennaPorts.N2 * config.pdsch_AntennaPorts.XP; AssertFatal(config.maxMIMO_layers != 0 && config.maxMIMO_layers <= tot_ant, "Invalid maxMIMO_layers %d\n", config.maxMIMO_layers); + LOG_I(GNB_APP, + "PDSCH DMRS additional position pos%d\n", + config.pdsch_dmrs_additional_position); config.redcap = get_redcap_config(0); config.ptrs = get_ptrs_config(); diff --git a/openair2/GNB_APP/gnb_paramdef.h b/openair2/GNB_APP/gnb_paramdef.h index 35a1c096d1..f2cc54401b 100644 --- a/openair2/GNB_APP/gnb_paramdef.h +++ b/openair2/GNB_APP/gnb_paramdef.h @@ -101,6 +101,7 @@ typedef enum { #define GNB_CONFIG_STRING_CONFIG_REP "CSI_report_type" #define GNB_CONFIG_STRING_1ST_ACTIVE_BWP "first_active_bwp" #define GNB_CONFIG_STRING_LIMIT_RSRP_REPORT "max_num_RSRP_reported" +#define GNB_CONFIG_STRING_PDSCH_DMRS_ADDITIONAL_POSITION "pdsch_dmrs_additional_position" #define GNB_CONFIG_HLP_STRING_ENABLE_SDAP "enable the SDAP layer\n" #define GNB_CONFIG_HLP_FORCE256QAMOFF "suppress activation of 256 QAM despite UE support" @@ -114,6 +115,7 @@ typedef enum { #define GNB_CONFIG_HLP_CU_SIBS "List of CU generated SIBs to be transmitted" #define GNB_CONFIG_HLP_DU_SIBS "List of DU generated SIBs to be transmitted" #define GNB_CONFIG_HLP_CONFIG_REP "Define quantity for CSI report (options: ssb_rsrp, ssb_sinr and cri_rsrp)" +#define GNB_CONFIG_HLP_PDSCH_DMRS_ADDITIONAL_POSITION "PDSCH DMRS additional position: 0=pos0, 1=pos1" /*-----------------------------------------------------------------------------------------------------------------------------------------*/ @@ -163,6 +165,7 @@ typedef enum { {GNB_CONFIG_STRING_CONFIG_REP, GNB_CONFIG_HLP_CONFIG_REP, 0, .strptr=NULL, .defstrval="ssb_rsrp", TYPE_STRING, 0}, \ {GNB_CONFIG_STRING_1ST_ACTIVE_BWP, NULL, 0, .iptr=NULL, .defintval=0, TYPE_INT, 0}, \ {GNB_CONFIG_STRING_LIMIT_RSRP_REPORT, NULL, 0, .iptr=NULL, .defintval=0, TYPE_INT, 0}, \ +{GNB_CONFIG_STRING_PDSCH_DMRS_ADDITIONAL_POSITION, GNB_CONFIG_HLP_PDSCH_DMRS_ADDITIONAL_POSITION, 0, .iptr=NULL, .defintval=1, TYPE_INT, 0}, \ } // clang-format on @@ -207,6 +210,7 @@ typedef enum { #define GNB_CONFIG_REP_IDX 37 #define GNB_1ST_ACTIVE_BWP_IDX 38 #define GNB_LIMIT_RSRP_REPORT_IDX 39 +#define GNB_PDSCH_DMRS_ADDITIONAL_POSITION_IDX 40 #define TRACKING_AREA_CODE_OKRANGE {0x0001,0xFFFD} #define NUM_DL_HARQ_OKVALUES {2,4,6,8,10,12,16,32} @@ -256,6 +260,7 @@ typedef enum { 3 } }, \ { .s5 = { NULL } }, \ { .s5 = { NULL } }, \ + { .s5 = { NULL } }, \ } /*-------------------------------------------------------------------------------------------------------------------------------------------------*/ diff --git a/openair2/LAYER2/NR_MAC_COMMON/nr_mac_common.c b/openair2/LAYER2/NR_MAC_COMMON/nr_mac_common.c index 632c6659f4..a3392a2940 100644 --- a/openair2/LAYER2/NR_MAC_COMMON/nr_mac_common.c +++ b/openair2/LAYER2/NR_MAC_COMMON/nr_mac_common.c @@ -4133,12 +4133,19 @@ uint16_t compute_pucch_prb_size(uint8_t format, nr_prbs); if (format==2){ + /* nr_decode_pucch2() requires at least two PRB-symbols. Preserve the + * one-symbol format-2 resource at symbol 13 (which does not overlap the + * usual PUSCH ending at symbol 12) and use frequency width to meet that + * decoder invariant. This common helper keeps UE TX and gNB RX aligned. */ + const int min_prbs = n_symb == 1 ? 2 : 1; + AssertFatal(nr_prbs >= min_prbs, + "PUCCH format 2 needs at least %d PRBs for %d symbol(s)", min_prbs, n_symb); // TODO fix this for multiple CSI reports - for (int i = nr_prbs; i > 0; i--) { + for (int i = nr_prbs; i >= min_prbs; i--) { // compute code rate factor for next prb value int next_prb_factor = (i - 1) * n_symb * Qm * n_re_ctrl * r; // if it does not sa - if (O_tot > next_prb_factor) + if (i == min_prbs || O_tot > next_prb_factor) return i; } } @@ -4552,8 +4559,10 @@ static void set_bitlen_size_singlepanel(CSI_Meas_bitlen_t *csi_bitlen, int n1, i case 7: case 8: if(n1 == 4 && n2 == 1) { - csi_bitlen->pmi_i11_bitlen[i] = ceil(log2(n1 * o1)); - csi_bitlen->pmi_i12_bitlen[i] = ceil(log2(n2 * o2 / 2)); + // TS 38.212 Table 6.3.1.1.2-1: only half of the horizontal + // oversampled beams are valid for rank 7/8 in the 4x1 geometry. + csi_bitlen->pmi_i11_bitlen[i] = ceil(log2(n1 * o1 / 2)); + csi_bitlen->pmi_i12_bitlen[i] = 0; csi_bitlen->pmi_i13_bitlen[i] = 0; csi_bitlen->pmi_x2_bitlen[i] = 1; } diff --git a/openair2/LAYER2/NR_MAC_UE/nr_ue_procedures.c b/openair2/LAYER2/NR_MAC_UE/nr_ue_procedures.c index 83a6693b23..b579b2d337 100644 --- a/openair2/LAYER2/NR_MAC_UE/nr_ue_procedures.c +++ b/openair2/LAYER2/NR_MAC_UE/nr_ue_procedures.c @@ -2371,7 +2371,8 @@ bool get_downlink_ack(NR_UE_MAC_INST_t *mac, frame_t frame, int slot, PUCCH_sche if (current_DL_BWP && current_DL_BWP->pdsch_Config && current_DL_BWP->pdsch_Config->maxNrofCodeWordsScheduledByDCI - && current_DL_BWP->pdsch_Config->maxNrofCodeWordsScheduledByDCI[0] == 2) { + && *current_DL_BWP->pdsch_Config->maxNrofCodeWordsScheduledByDCI + == NR_PDSCH_Config__maxNrofCodeWordsScheduledByDCI_n2) { two_transport_blocks = true; number_of_code_word = 2; } @@ -2964,6 +2965,11 @@ static nfapi_nr_ue_csi_payload_t get_csirs_RI_PMI_CQI_payload(NR_UE_MAC_INST_t * int pmi_x1_bitlen = csi_report->csi_meas_bitlen.pmi_x1_bitlen[mac->csirs_measurements.ri]; int pmi_x2_bitlen = csi_report->csi_meas_bitlen.pmi_x2_bitlen[mac->csirs_measurements.ri]; int cqi_bitlen = csi_report->csi_meas_bitlen.cqi_bitlen[mac->csirs_measurements.ri]; + /* For ranks with two transport blocks, the report carries one + * wideband CQI per codeword. Until separate per-codeword estimates + * are available, report the measured wideband CQI for both. */ + const uint16_t cqi = mac->csirs_measurements.cqi & 0x0f; + const uint16_t cqi_payload = cqi_bitlen > 4 ? (cqi << 4) | cqi : cqi; int padding_bitlen = 0; // TODO: Improvements will be needed to cri_bitlen>0 and pmi_x1_bitlen>0 if (mapping_type == ON_PUSCH) { @@ -2971,7 +2977,7 @@ static nfapi_nr_ue_csi_payload_t get_csirs_RI_PMI_CQI_payload(NR_UE_MAC_INST_t * p2_bits = pmi_x1_bitlen + pmi_x2_bitlen; temp_payload_1 = (0/*mac->csi_measurements.cri*/ << (cqi_bitlen + ri_bitlen)) | (mac->csirs_measurements.ri << cqi_bitlen) | - (mac->csirs_measurements.cqi); + cqi_payload; temp_payload_2 = (mac->csirs_measurements.i1 << pmi_x2_bitlen) | mac->csirs_measurements.i2; } @@ -2982,7 +2988,7 @@ static nfapi_nr_ue_csi_payload_t get_csirs_RI_PMI_CQI_payload(NR_UE_MAC_INST_t * (mac->csirs_measurements.ri << (cqi_bitlen + pmi_x2_bitlen + pmi_x1_bitlen + padding_bitlen)) | (mac->csirs_measurements.i1 << (cqi_bitlen + pmi_x2_bitlen)) | (mac->csirs_measurements.i2 << (cqi_bitlen)) | - (mac->csirs_measurements.cqi); + cqi_payload; } temp_payload_1 = reverse_bits(temp_payload_1, p1_bits); diff --git a/openair2/LAYER2/NR_MAC_UE/nr_ue_scheduler.c b/openair2/LAYER2/NR_MAC_UE/nr_ue_scheduler.c index f952bfe791..d00502a5e7 100644 --- a/openair2/LAYER2/NR_MAC_UE/nr_ue_scheduler.c +++ b/openair2/LAYER2/NR_MAC_UE/nr_ue_scheduler.c @@ -1154,15 +1154,36 @@ static NR_CSI_ResourceConfigId_t find_CSI_resourceconfig(NR_CSI_MeasConfig_t *cs return -1; // not found any CSI-resource in current DL BWP associated with this CSI-RS ID } -static uint8_t set_csirs_measurement_bitmap(NR_CSI_MeasConfig_t *csi_measconfig, NR_CSI_ResourceConfigId_t csi_res_id) +static uint8_t set_csirs_measurement_bitmap(const NR_UE_MAC_INST_t *mac, + NR_CSI_MeasConfig_t *csi_measconfig, + NR_CSI_ResourceConfigId_t csi_res_id, + fapi_nr_dl_config_csirs_pdu_rel15_t *csirs_config_pdu) { uint8_t meas_bitmap = 0; + csirs_config_pdu->codebook_n1 = 0; + csirs_config_pdu->codebook_n2 = 0; + csirs_config_pdu->codebook_mode = 0; if (csi_res_id > NR_maxNrofCSI_ResourceConfigurations) return meas_bitmap; // CSI-RS for tracking for(int i = 0; i < csi_measconfig->csi_ReportConfigToAddModList->list.count; i++) { NR_CSI_ReportConfig_t *report_config = csi_measconfig->csi_ReportConfigToAddModList->list.array[i]; if(report_config->resourcesForChannelMeasurement != csi_res_id) continue; + const nr_csi_report_t *report = &mac->csi_report_template[i]; + if (report->reportConfigId == report_config->reportConfigId && report->N1 > 0 && report->N2 > 0) { + if (csirs_config_pdu->codebook_n1 != 0 + && (csirs_config_pdu->codebook_n1 != report->N1 + || csirs_config_pdu->codebook_n2 != report->N2 + || csirs_config_pdu->codebook_mode != report->codebook_mode)) { + LOG_W(NR_MAC, + "CSI-RS resource %ld is associated with conflicting PMI codebooks; retaining the first one\n", + csi_res_id); + } else { + csirs_config_pdu->codebook_n1 = report->N1; + csirs_config_pdu->codebook_n2 = report->N2; + csirs_config_pdu->codebook_mode = report->codebook_mode; + } + } // bit 0 RSRP bit 1 RI bit 2 LI bit 3 PMI bit 4 CQI bit 5 i1 switch (report_config->reportQuantity.present) { case NR_CSI_ReportConfig__reportQuantity_PR_cri_RI_PMI_CQI: @@ -1220,7 +1241,8 @@ static void nr_schedule_csirs_reception(NR_UE_MAC_INST_t *mac, int frame, int sl continue; LOG_D(MAC,"Scheduling reception of CSI-RS in frame %d slot %d\n", frame, slot); fapi_nr_dl_config_csirs_pdu_rel15_t *csirs_config_pdu = &dl_config->dl_config_list[dl_config->number_pdus].csirs_config_pdu.csirs_config_rel15; - csirs_config_pdu->measurement_bitmap = set_csirs_measurement_bitmap(csi_measconfig, csi_res_id); + csirs_config_pdu->measurement_bitmap = + set_csirs_measurement_bitmap(mac, csi_measconfig, csi_res_id, csirs_config_pdu); csirs_config_pdu->subcarrier_spacing = mu; csirs_config_pdu->cyclic_prefix = current_DL_BWP->cyclicprefix ? *current_DL_BWP->cyclicprefix : 0; diff --git a/openair2/LAYER2/NR_MAC_gNB/config.c b/openair2/LAYER2/NR_MAC_gNB/config.c index 8e089a2b05..5f86124266 100644 --- a/openair2/LAYER2/NR_MAC_gNB/config.c +++ b/openair2/LAYER2/NR_MAC_gNB/config.c @@ -186,6 +186,28 @@ int precoding_weigths_generation(nfapi_nr_pm_list_t *mat, return pmiq; } +static int precoding_weights_generation_4x1_high_rank(nfapi_nr_pm_list_t *mat, + int pmiq, + int layers) +{ + const uint8_t i11_count = nr_type1_sp_4x1_i11_count(layers); + for (uint8_t i11 = 0; i11 < i11_count; i11++) { + for (uint8_t i2 = 0; i2 < 2; i2++) { + nfapi_nr_pm_pdu_t *pdu = &mat->pmi_pdu[pmiq]; + pdu->pm_idx = pmiq + 1; // index zero is the identity matrix + pdu->numLayers = layers; + pdu->num_ant_ports = 8; + AssertFatal(nr_type1_sp_4x1_weights(layers, i11, 0, i2, pdu->weights), + "Invalid rank-%d Type-I single-panel PMI i11=%u i2=%u\n", + layers, + i11, + i2); + pmiq++; + } + } + return pmiq; +} + nfapi_nr_pm_list_t init_DL_MIMO_codebook(gNB_MAC_INST *gNB, nr_pdsch_AntennaPorts_t antenna_ports) { int num_antenna_ports = antenna_ports.N1 * antenna_ports.N2 * antenna_ports.XP; @@ -208,20 +230,32 @@ nfapi_nr_pm_list_t init_DL_MIMO_codebook(gNB_MAC_INST *gNB, nr_pdsch_AntennaPort int O1 = num_antenna_ports > 2 ? 4 : 1; //Horizontal beam oversampling (1 or 4) 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); + const int low_codebook_layers = min(max_mimo_layers, 4); + const bool high_rank_4x1 = num_antenna_ports == 8 && antenna_ports.XP == 2 && N1 == 4 && N2 == 1; 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 < codebook_layers; i++) { + memset(gNB->precoding_matrix_size, 0, sizeof(gNB->precoding_matrix_size)); + for (int i = 0; i < low_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; mat.num_pm_idx += gNB->precoding_matrix_size[i]; } + if (max_mimo_layers > 4 && !high_rank_4x1) { + LOG_W(NR_MAC, + "Rank-5--8 PMI is only implemented for the eight-port Type-I single-panel 4x1 geometry; " + "unsupported geometry XP=%d N1=%d N2=%d will use identity precoding\n", + antenna_ports.XP, + N1, + N2); + } else if (high_rank_4x1) { + for (int layers = 5; layers <= max_mimo_layers; layers++) { + gNB->precoding_matrix_size[layers - 1] = 2 * nr_type1_sp_4x1_i11_count(layers); + mat.num_pm_idx += gNB->precoding_matrix_size[layers - 1]; + } + } mat.pmi_pdu = malloc16(mat.num_pm_idx * sizeof(*mat.pmi_pdu)); AssertFatal(mat.pmi_pdu != NULL, "out of memory\n"); @@ -277,8 +311,16 @@ nfapi_nr_pm_list_t init_DL_MIMO_codebook(gNB_MAC_INST *gNB, nr_pdsch_AntennaPort } int pmiq = 0; - for (int layers = 1; layers <= codebook_layers; layers++) + for (int layers = 1; layers <= low_codebook_layers; layers++) pmiq = precoding_weigths_generation(&mat, pmiq, layers, N1, N2, O1, O2, num_antenna_ports, theta_n, v_lm); + if (high_rank_4x1) + for (int layers = 5; layers <= max_mimo_layers; layers++) + pmiq = precoding_weights_generation_4x1_high_rank(&mat, pmiq, layers); + + AssertFatal(pmiq == mat.num_pm_idx, + "Generated %d precoding matrices but allocated %d\n", + pmiq, + mat.num_pm_idx); return mat; } diff --git a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler.c b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler.c index ab2588a25b..537785283a 100644 --- a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler.c +++ b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler.c @@ -115,6 +115,51 @@ static void copy_ul_tti_req(nfapi_nr_ul_tti_request_t *to, nfapi_nr_ul_tti_reque to->groups_list[i] = from->groups_list[i]; } +static bool has_incompatible_dl_harq(gNB_MAC_INST *gNB, int target_layers) +{ + UE_iterator(gNB->UE_info.connected_ue_list, UE) { + const NR_UE_sched_ctrl_t *sched_ctrl = &UE->UE_sched_ctrl; + for (int pid = 0; pid < NR_MAX_HARQ_PROCESSES; ++pid) { + const NR_UE_harq_t *harq = &sched_ctrl->harq_processes[pid]; + const bool in_flight = harq->is_waiting || harq->round > 0; + if (in_flight && harq->sched_pdsch.nrOfLayers > target_layers) { + LOG_D(NR_MAC, + "[UE %04x] defer TX-mask downshift: HARQ PID %d still uses %d layers (target %d)\n", + UE->rnti, + pid, + harq->sched_pdsch.nrOfLayers, + target_layers); + return true; + } + } + } + return false; +} + +/* A physical downshift cannot occur while a buffered retransmission needs + * more layers (and possibly a second codeword) than the target supports. + * The control thread has already made the target visible to new scheduling, + * so the old high-rank HARQ set can only drain from this point onward. */ +static void apply_pending_tx_mask_if_harq_safe(gNB_MAC_INST *gNB) +{ + nr_antenna_state_t *state = gNB->antenna_state; + uint8_t target_mask = 0; + if (!nr_get_pending_tx_antenna_bitmask(state, &target_mask)) + return; + + int target_layers = 0; + for (uint8_t mask = target_mask; mask != 0; mask >>= 1) + target_layers += mask & 1u; + if (has_incompatible_dl_harq(gNB, target_layers)) + return; + + if (nr_apply_requested_tx_antenna_bitmask(state, target_mask)) + LOG_I(NR_MAC, + "Applied HARQ-safe TX-mask downshift to 0x%02X (%d layers)\n", + target_mask, + target_layers); +} + void gNB_dlsch_ulsch_scheduler(module_id_t module_idP, frame_t frame, slot_t slot, NR_Sched_Rsp_t *sched_info) { protocol_ctxt_t ctxt = {0}; @@ -125,6 +170,7 @@ void gNB_dlsch_ulsch_scheduler(module_id_t module_idP, frame_t frame, slot_t slo NR_ServingCellConfigCommon_t *scc = cc->ServingCellConfigCommon; NR_SCHED_LOCK(&gNB->sched_lock); + apply_pending_tx_mask_if_harq_safe(gNB); int slots_frame = gNB->frame_structure.numb_slots_frame; clear_beam_information(&gNB->beam_info, frame, slot, slots_frame); diff --git a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_dlsch.c b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_dlsch.c index 451643972d..8424616794 100644 --- a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_dlsch.c +++ b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_dlsch.c @@ -13,6 +13,9 @@ #include "LAYER2/NR_MAC_gNB/mac_proto.h" #include "openair2/LAYER2/nr_rlc/nr_rlc_oai_api.h" +#include +#include + /*TAG*/ #include "NR_TAG-Id.h" @@ -348,6 +351,9 @@ void finish_nr_dl_harq(NR_UE_sched_ctrl_t *sched_ctrl, int harq_pid) NR_UE_harq_t *harq = &sched_ctrl->harq_processes[harq_pid]; harq->ndi ^= 1; + if (harq->sched_pdsch.nrOfLayers > 4) + harq->ndi2 ^= 1; + harq->pending_cw_mask = 0; harq->round = 0; add_tail_nr_list(&sched_ctrl->available_dl_harq, harq_pid); @@ -451,14 +457,23 @@ 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(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); + const int layers = new_sched.nrOfLayers; - // If the RI changed between current rtx and a previous transmission - // we need to verify if it is not decreased - // othwise it wouldn't be possible to transmit the same TBS - layers = (layers < new_sched.nrOfLayers) ? layers : new_sched.nrOfLayers; - pm_index = (layers < new_sched.nrOfLayers) ? pm_index : new_sched.pm_index; + /* HARQ retransmission must retain the original layer mapping, codeword + * count, and TBS. Recomputing it from the current RI/TX target can turn a + * rank-8 two-codeword HARQ process into a one-codeword transmission and + * silently discard TB2. Normally a staged mask downshift keeps enough + * physical chains active until this process drains; if that mechanism was + * bypassed, defer instead of emitting an invalid retransmission. */ + if (nr_mac->antenna_state != NULL) { + const int active_tx = nr_get_active_tx_antenna_count(nr_mac->antenna_state); + if (active_tx < layers) { + LOG_W(NR_MAC, + "[UE %04x][%4d.%2d] defer DL HARQ PID %d: original rank %d exceeds %d active TX chains\n", + UE->rnti, frame, slot, current_harq_pid, layers, active_tx); + return false; + } + } const int coresetid = sched_ctrl->coreset->controlResourceSetId; const int tda = get_dl_tda(nr_mac, slot); @@ -556,7 +571,6 @@ static bool allocate_dl_retransmission(gNB_MAC_INST *nr_mac, 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); @@ -586,11 +600,8 @@ 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; - } + /* DAI counts PDSCH assignments, not transport blocks. A two-codeword + * PDSCH uses one PUCCH allocation carrying two HARQ-ACK bits. */ 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; @@ -659,6 +670,24 @@ static int comparator(const void *p, const void *q) return 0; } +/* Keep latency bounded under multi-UE load by preventing the first PF-selected + * UE from consuming the complete contiguous PRB region. The ordinary OAI PF + * loop already supports several frequency-disjoint PDSCHs per slot, but a UE + * with an accumulated RLC burst can otherwise take every PRB and make the + * remaining UEs wait for later DL slots. This switch only divides resources + * when at least two eligible UEs remain; single-UE peak throughput is unchanged. */ +static bool dl_fair_rb_cap_enabled(void) +{ + static int enabled = -1; + if (enabled < 0) { + const char *setting = getenv("OAI_DL_FAIR_RB_CAP"); + enabled = setting != NULL && atoi(setting) != 0; + if (enabled) + LOG_I(NR_MAC, "DL fair-RB cap enabled for multi-UE low-latency scheduling\n"); + } + return enabled; +} + static void pf_dl(gNB_MAC_INST *mac, post_process_pdsch_t *pp_pdsch, NR_UE_info_t **UE_list, @@ -744,7 +773,13 @@ static void pf_dl(gNB_MAC_INST *mac, /* Calculate coeff */ const NR_bler_options_t *bo = &mac->dl_bler; const int max_mcs_table = current_BWP->mcsTableIdx == 1 ? 27 : 28; - const int max_mcs = min(sched_ctrl->dl_max_mcs, max_mcs_table); + /* The experimental identity-precoded rank-8 path can make a Type-I + * codebook CQI pessimistic for the actual PDSCH precoder. Keep CQI as + * the default cap, but allow a bounded HARQ/BLER probe when explicitly + * requested. The configured min/max MCS still apply in either mode. */ + const char *mcs_policy = getenv("OAI_DL_MCS_POLICY"); + const bool bler_only = mcs_policy != NULL && strcmp(mcs_policy, "bler_only") == 0; + const int max_mcs = min(bler_only ? bo->max_mcs : sched_ctrl->dl_max_mcs, max_mcs_table); int selected_mcs; if (bo->harq_round_max == 1) { int new_mcs = min(bo->max_mcs, max_mcs); @@ -816,6 +851,16 @@ static void pf_dl(gNB_MAC_INST *mac, continue; } + int eligible_ues_left = 0; + if (dl_fair_rb_cap_enabled()) { + for (UEsched_t *candidate = iterator; candidate->UE != NULL; candidate++) { + NR_UE_sched_ctrl_t *candidate_ctrl = &candidate->UE->UE_sched_ctrl; + if (nr_mac_ue_is_active(candidate->UE) + && candidate_ctrl->available_dl_harq.head >= 0) + eligible_ues_left++; + } + } + /* MCS has been set above */ int tda = get_dl_tda(mac, slot); AssertFatal(tda >= 0,"Unable to find PDSCH time domain allocation in list\n"); @@ -852,6 +897,15 @@ static void pf_dl(gNB_MAC_INST *mac, iterator++; continue; } + if (eligible_ues_left > 1) { + const int fair_ues = min(remainUEs[beam.idx], eligible_ues_left); + if (fair_ues > 1) { + /* Recompute after every allocation: unused shares from a lightly + * loaded UE remain available to later UEs in this same slot. */ + const int fair_rb_cap = (n_rb_sched[beam.idx] + fair_ues - 1) / fair_ues; + max_rbSize = min(max_rbSize, max(min_rbSize, fair_rb_cap)); + } + } int CCEIndex = get_cce_index(mac, CC_id, @@ -879,12 +933,8 @@ static void pf_dl(gNB_MAC_INST *mac, 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; - } + /* One PDSCH assignment has one DAI increment even when it carries two + * codewords; the UE multiplexes both ACK bits on this allocation. */ 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); @@ -1151,6 +1201,13 @@ static dlsch_fill_result_t fill_dlsch_transport_block(gNB_MAC_INST *mac, return result; } +static void fill_padding_only_transport_block(uint8_t *tb, uint32_t tbs) +{ + DevAssert(tbs >= sizeof(NR_MAC_SUBHEADER_FIXED)); + memset(tb, 0, tbs); + ((NR_MAC_SUBHEADER_FIXED *)tb)->LCID = DL_SCH_LCID_PADDING; +} + 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; @@ -1191,6 +1248,19 @@ void post_process_dlsch(gNB_MAC_INST *nr_mac, post_process_pdsch_t *pdsch, NR_UE NR_tda_info_t *tda_info = &sched_pdsch->tda_info; NR_pdsch_dmrs_t *dmrs_parms = &sched_pdsch->dmrs_parms; NR_UE_harq_t *harq = &sched_ctrl->harq_processes[current_harq_pid]; + const int active_codewords = nr_pdsch_num_codewords(nrOfLayers); + if (harq->round == 0) { + harq->pending_cw_mask = (1U << active_codewords) - 1; + } else if (active_codewords == 2) { + DevAssert(harq->pending_cw_mask > 0 && harq->pending_cw_mask < 4); + /* A codeword already ACKed must not be sent again with its old NDI: + * the UE would otherwise deliver its MAC SDUs twice. Give its padding + * replacement a fresh NDI and RV0 while the peer TB is retransmitted. */ + if (!(harq->pending_cw_mask & 1)) + harq->ndi ^= 1; + if (!(harq->pending_cw_mask & 2)) + harq->ndi2 ^= 1; + } NR_sched_pucch_t *pucch = NULL; DevAssert(!harq->is_waiting); if (sched_pdsch->pucch_allocation >= 0) { @@ -1202,6 +1272,28 @@ void post_process_dlsch(gNB_MAC_INST *nr_mac, post_process_pdsch_t *pdsch, NR_UE } UE->mac_stats.dl.rounds[harq->round]++; int tpc = nr_mac_get_tpc(&sched_ctrl->pucch_pc); + if (nrOfLayers > 4 && sched_pdsch->rbSize >= 100) { + static bool logged_full_high_rank_shape[32] = {false}; + if (!__atomic_exchange_n(&logged_full_high_rank_shape[sched_pdsch->mcs], true, __ATOMIC_RELAXED)) + LOG_I(NR_MAC, + "[HIGH_RANK_SCHEDULE] layers=%u RB=%u symbols=%u start=%u " + "MCS=%u Qm=%u R=%u TBS=%u/%u total=%u DMRS_mask=0x%x " + "DMRS_RE_per_RB=%u DMRS_symbols=%u CDM_groups=%u\n", + nrOfLayers, + sched_pdsch->rbSize, + tda_info->nrOfSymbols, + tda_info->startSymbolIndex, + sched_pdsch->mcs, + sched_pdsch->Qm, + sched_pdsch->R, + TBS, + TBS2, + TBS + TBS2, + dmrs_parms->dl_dmrs_symb_pos, + dmrs_parms->N_PRB_DMRS, + dmrs_parms->N_DMRS_SLOT, + dmrs_parms->numDmrsCdmGrpsNoData); + } LOG_D(NR_MAC, "%4d.%2d [DLSCH/PDSCH/PUCCH] RNTI %04x DCI L %d start %3d RBs %3d startSymbol %2d nb_symbol %2d dmrspos %x MCS %2d nrOfLayers %d TBS %4d HARQ PID %2d round %d RV %d NDI %d dl_data_to_ULACK %d (%d.%d) PUCCH allocation %d TPC %d\n", frame, @@ -1279,6 +1371,10 @@ void post_process_dlsch(gNB_MAC_INST *nr_mac, post_process_pdsch_t *pdsch, NR_UE fapi_beam, nl_tbslbrm, pduindex); + if (harq->round > 0 && active_codewords == 2) + for (int q = 0; q < active_codewords; q++) + if (!(harq->pending_cw_mask & (1U << q))) + pdsch_pdu->rvIndex[q] = 0; LOG_D(NR_MAC,"Configuring DCI/PDCCH in %d.%d at CCE %d, rnti %x\n", frame,slot,sched_ctrl->cce_index,rnti); @@ -1375,6 +1471,15 @@ void post_process_dlsch(gNB_MAC_INST *nr_mac, post_process_pdsch_t *pdsch, NR_UE "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", current_harq_pid); + if (active_codewords == 2) + for (int q = 0; q < active_codewords; q++) { + if (harq->pending_cw_mask & (1U << q)) + continue; + byte_array_t *transport_block = q == 0 ? &harq->transportBlock : &harq->transportBlock2; + const uint32_t cw_tbs = q == 0 ? TBS : TBS2; + uint8_t *buf = allocate_transportBlock_buffer(transport_block, cw_tbs); + fill_padding_only_transport_block(buf, cw_tbs); + } T(T_GNB_MAC_RETRANSMISSION_DL_PDU_WITH_DATA, T_INT(module_id), T_INT(CC_id), T_INT(rnti), T_INT(frame), T_INT(slot), T_INT(current_harq_pid), T_INT(harq->round), T_BUFFER(harq->transportBlock.buf, TBS)); UE->mac_stats.dl.total_rbs_retx += sched_pdsch->rbSize; diff --git a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_primitives.c b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_primitives.c index d7d5484d00..5cf56d173d 100644 --- a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_primitives.c +++ b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_primitives.c @@ -164,8 +164,10 @@ 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 || layers > 4) + 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) + if (layers < 1 || layers > NR_MAX_NB_LAYERS || nrmac->precoding_matrix_size[layers - 1] == 0) + return 0; const NR_UE_sched_ctrl_t *sched_ctrl = &UE->UE_sched_ctrl; int reported_ri_layers = sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.ri + 1; @@ -180,9 +182,17 @@ uint16_t get_pm_index(const gNB_MAC_INST *nrmac, 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 active_tx = nr_get_active_tx_antenna_count(ant_state); + if (active_tx < ant_state->num_tx_antennas) { + // A full-panel codebook spreads every layer over ports that the RU + // subsequently blanks. Identity mapping instead places the + // reduced layer count one-to-one on the remaining active ports. + LOG_D(NR_MAC, + "[ANTENNA_MUTE] %d/%u TX ports active, using identity matrix\n", + active_tx, + ant_state->num_tx_antennas); + return 0; + } } const int report_id = sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.csi_report_id; @@ -191,6 +201,13 @@ uint16_t get_pm_index(const gNB_MAC_INST *nrmac, const int N2 = csi_report->N2; const int antenna_ports = (N1 * N2) << 1; if (antenna_ports < 2) return 0; // single antenna port + if (antenna_ports == 8 && (layers == 3 || layers == 4)) { + LOG_D(NR_MAC, + "Using identity matrix for rank-%d over eight ports: the legacy UE rank-3/4 detector " + "does not support non-identity eight-port precoding\n", + layers); + return 0; + } 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; @@ -226,17 +243,38 @@ uint16_t get_pm_index(const gNB_MAC_INST *nrmac, 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); + if (layers > 4) { + const int i11_count = nr_type1_sp_4x1_i11_count(layers); + if (antenna_ports != 8 || N1 != 4 || N2 != 1 || i11_count == 0 + || i11 >= i11_count || i12 != 0 || i13 != 0 || i2 < 0 || i2 >= 2) { + LOG_W(NR_MAC, + "Unsupported rank-%d high-rank PMI for N1=%d N2=%d: i11=%d i12=%d i13=%d i2=%d; " + "using identity matrix\n", + layers, + N1, + N2, + i11, + i12, + i13, + i2); + return 0; + } + // High-rank matrices are generated in i11-major, i2-minor order. + lay_index = i11 * 2 + i2; + } else { + int k1, k2; + get_k1_k2_indices(layers, N1, N2, i13, &k1, &k2); // indices for the generated PHY matrix + const int O2 = N2 == 1 ? 1 : 4; + const int O1 = 4; // horizontal beam oversampling for more than two antenna ports + const int max_i2 = layers == 1 ? 4 : 2; + int K1, K2; + get_K1_K2(N1, N2, &K1, &K2, layers); + // computing precoding matrix index according to init_DL_MIMO_codebook ordering + 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]; @@ -860,7 +898,12 @@ int get_mcs_from_bler(const NR_bler_options_t *bler_options, if (diff < 0) // wrap around diff += 1024; - max_mcs = min(max_mcs, bler_options->max_mcs); + /* Keep the configured interval valid even when CSI supplies a lower cap. + * In particular, min_mcs == max_mcs is an explicit fixed-MCS request and + * must not alternate between the CQI fallback and the configured value at + * each BLER update boundary. */ + max_mcs = max((int)bler_options->min_mcs, + min(max_mcs, (int)bler_options->max_mcs)); const uint8_t old_mcs = min(bler_stats->mcs, max_mcs); if (diff < BLER_UPDATE_FRAME) return old_mcs; // no update @@ -971,8 +1014,17 @@ dci_pdu_rel15_t prepare_dci_dl_payload(const gNB_MAC_INST *gNB_mac, 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.ndi2.val = harq->ndi2; dci_payload.rv2.val = pdsch_pdu->rvIndex[1]; + } else { + /* maxNrofCodeWordsScheduledByDCI=n2 keeps the second-TB fields in DCI + * format 1_1 even for a one-codeword (rank <= 4) PDSCH. TS 38.214 + * disables that transport block with I_MCS=26 and RV=1. Leaving the + * zero-initialized values here makes the UE treat CW1 as active and + * interpret the antenna-port row through the two-codeword DMRS table. */ + dci_payload.mcs2.val = 26; + dci_payload.ndi2.val = 0; + dci_payload.rv2.val = 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 diff --git a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_uci.c b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_uci.c index 7840b3d974..6462095b60 100644 --- a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_uci.c +++ b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_uci.c @@ -15,6 +15,24 @@ #include "common/ran_context.h" #include "common/utils/nr/nr_common.h" #include "nfapi/oai_integration/vendor_ext.h" + +/* DAI counts PDSCH assignments, while O_ACK counts HARQ-ACK bits. With + * maxNrofCodeWordsScheduledByDCI=n2, the type-2 codebook reserves two ACK + * bits per assignment even when DCI disables the second transport block. + * The reserved bit still occupies the UCI payload, but it must not turn a + * successfully decoded one-codeword PDSCH into a HARQ failure. */ +static uint8_t harq_ack_bits_per_assignment(const NR_UE_info_t *UE, bool is_common) +{ + if (is_common) + return 1; + const NR_PDSCH_Config_t *pdsch_config = UE->current_DL_BWP.pdsch_Config; + return pdsch_config != NULL && pdsch_config->maxNrofCodeWordsScheduledByDCI != NULL + && *pdsch_config->maxNrofCodeWordsScheduledByDCI + == NR_PDSCH_Config__maxNrofCodeWordsScheduledByDCI_n2 + ? 2 + : 1; +} + static void nr_fill_nfapi_pucch(gNB_MAC_INST *nrmac, frame_t frame, slot_t slot, const NR_sched_pucch_t *pucch, NR_UE_info_t* UE) { @@ -48,14 +66,15 @@ static void nr_fill_nfapi_pucch(gNB_MAC_INST *nrmac, frame_t frame, slot_t slot, future_ul_tti_req->n_pdus += 1; LOG_D(NR_MAC, - "%s %4d.%2d Scheduling pucch reception in %4d.%2d: bits SR %d, DAI %d, CSI %d on res %d\n", - pucch->dai_c>0 ? "pucch_acknack" : "", + "%s %4d.%2d Scheduling pucch reception in %4d.%2d: bits SR %d, DAI %d, HARQ-ACK %d, CSI %d on res %d\n", + pucch->harq_ack_bits > 0 ? "pucch_acknack" : "", frame, slot, pucch->frame, pucch->ul_slot, pucch->sr_flag, pucch->dai_c, + pucch->harq_ack_bits, pucch->csi_bits, pucch->resource_indicator); NR_COMMON_channels_t * common_ch=nrmac->common_channels; @@ -75,7 +94,7 @@ static void nr_fill_nfapi_pucch(gNB_MAC_INST *nrmac, frame_t frame, slot_t slot, UE, pucch->resource_indicator, pucch->csi_bits, - pucch->dai_c, + pucch->harq_ack_bits, pucch->sr_flag, pucch->r_pucch, nrmac->beam_info.beam_mode); @@ -151,7 +170,7 @@ static void schedule_pucch_core(gNB_MAC_INST *nrmac, NR_UE_info_t *UE, frame_t f return; } - const uint16_t O_ack = curr_pucch->dai_c; + const uint16_t O_ack = curr_pucch->harq_ack_bits; const uint16_t O_csi = curr_pucch->csi_bits; const uint8_t O_sr = curr_pucch->sr_flag; LOG_D(NR_MAC, @@ -359,10 +378,15 @@ int get_pucch_resourceid(NR_PUCCH_Config_t *pucch_Config, int O_uci, int pucch_r return *resource_id; } -static void handle_dl_harq(gNB_MAC_INST *mac, NR_UE_info_t * UE, int8_t harq_pid, bool success, int harq_round_max) +static void handle_dl_harq(gNB_MAC_INST *mac, NR_UE_info_t * UE, int8_t harq_pid, uint8_t ack_mask, int harq_round_max) { NR_UE_sched_ctrl_t *sched_ctrl = &UE->UE_sched_ctrl; NR_UE_harq_t *harq = &sched_ctrl->harq_processes[harq_pid]; + const uint8_t active_mask = harq->sched_pdsch.nrOfLayers > 4 ? 3 : 1; + harq->pending_cw_mask = nr_pdsch_pending_after_ack(harq->pending_cw_mask, active_mask, ack_mask); + LOG_D(NR_MAC, "DL HARQ UE %04x PID %d ACK mask 0x%x pending CW mask 0x%x round %u\n", + UE->rnti, harq_pid, ack_mask, harq->pending_cw_mask, harq->round); + const bool success = harq->pending_cw_mask == 0; harq->feedback_slot = -1; harq->is_waiting = false; if (success) { @@ -633,7 +657,7 @@ static void evaluate_cqi_report(uint8_t *payload, sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.wb_cqi_1tb = temp_cqi; LOG_D(MAC,"Wide-band CQI for the first TB %d\n", temp_cqi); if (cqi_bitlen > 4) { - temp_cqi = pickandreverse_bits(payload, 4, cumul_bits); + temp_cqi = pickandreverse_bits(payload, 4, cumul_bits + 4); sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.wb_cqi_2tb = temp_cqi; LOG_D(MAC,"Wide-band CQI for the second TB %d\n", temp_cqi); } @@ -886,7 +910,7 @@ static NR_UE_harq_t *find_harq(frame_t frame, slot_t slot, NR_UE_info_t * UE, in frame, slot); remove_front_nr_list(&sched_ctrl->feedback_dl_harq); - handle_dl_harq(NULL, UE, pid, false, harq_round_max); + handle_dl_harq(NULL, UE, pid, 0, harq_round_max); pid = sched_ctrl->feedback_dl_harq.head; if (pid < 0) return NULL; @@ -936,15 +960,28 @@ 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 int n_ack_bits = harq_ack_bits_per_assignment(UE, is_ra); + /* Msg4/common HARQ entries can carry the legacy zero layer sentinel. + * Preserve the scheduler's established 0--4 => one-codeword rule. */ + const int active_codewords = 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); - bool success = harq_confidence == 0 && harq_bit + n_ack_bits <= uci_01->harq.num_harq; + const bool feedback_valid = harq_confidence == 0 && harq_bit + n_ack_bits <= uci_01->harq.num_harq; + bool success = feedback_valid; + uint8_t ack_mask = 0; 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; + LOG_D(NR_MAC, "%4d.%2d bit %d pid %d cw %d active %d ack/nack %d\n", + frame, slot, harq_bit + q, pid, q, q < active_codewords, harq_value); + if (q < active_codewords) { + const bool ack = harq_value == 0; + success &= ack; + if (feedback_valid && ack) + ack_mask |= 1U << q; + } } + if (active_codewords == 2) + success = feedback_valid && (harq->pending_cw_mask & ~ack_mask) == 0; nr_mac_update_pdcch_closed_loop_adjust(sched_ctrl, 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) { @@ -953,7 +990,7 @@ void handle_nr_uci_pucch_0_1(module_id_t mod_id, frame_t frame, slot_t slot, con nr_timer_start(&sched_ctrl->tci_beam_switch); harq->start_tci_timer = false; } - handle_dl_harq(nrmac, UE, pid, success, nrmac->dl_bler.harq_round_max); + handle_dl_harq(nrmac, UE, pid, ack_mask, nrmac->dl_bler.harq_round_max); if (is_ra) { bool ue_rejected = nr_check_Msg4_MsgB_Ack(mod_id, frame, slot, UE, success); if (ue_rejected) { @@ -1042,16 +1079,34 @@ void handle_nr_uci_pucch_2_3_4(module_id_t mod_id, frame_t frame, slot_t slot, c break; } DevAssert(harq->is_waiting); - const int n_ack_bits = harq->sched_pdsch.nrOfLayers > 4 ? 2 : 1; + const int n_ack_bits = harq_ack_bits_per_assignment(UE, false); + /* Msg4/common HARQ entries can carry the legacy zero layer sentinel. + * Preserve the scheduler's established 0--4 => one-codeword rule. */ + const int active_codewords = 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); - bool success = uci_234->harq.harq_crc != 1 && harq_bit + n_ack_bits <= uci_234->harq.harq_bit_len; + const bool feedback_valid = uci_234->harq.harq_crc != 1 && harq_bit + n_ack_bits <= uci_234->harq.harq_bit_len; + bool success = feedback_valid; + uint8_t ack_mask = 0; 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; + const int logical_bit = harq_bit + q; + /* get_downlink_ack() reverses the Type-2 codebook before PUCCH + * encoding. PUCCH formats 2/3/4 expose that wire order directly, + * unlike the format-0/1 harq_list. Convert it back before assigning + * bits to DAI/codeword positions. This matters when CW1 is disabled: + * the reserved NACK and the active CW0 ACK otherwise exchange roles. */ + const int payload_bit = uci_234->harq.harq_bit_len - 1 - logical_bit; + const int acknack = (uci_234->harq.harq_payload[payload_bit >> 3] >> (payload_bit & 7)) & 0x01; + LOG_D(NR_MAC, "%4d.%2d logical bit %d payload bit %d pid %d cw %d active %d ack/nack %d\n", + frame, slot, logical_bit, payload_bit, pid, q, q < active_codewords, acknack); + if (q < active_codewords) { + success &= acknack; + if (feedback_valid && acknack) + ack_mask |= 1U << q; + } } + if (active_codewords == 2) + success = feedback_valid && (harq->pending_cw_mask & ~ack_mask) == 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; @@ -1059,7 +1114,7 @@ void handle_nr_uci_pucch_2_3_4(module_id_t mod_id, frame_t frame, slot_t slot, c nr_timer_start(&sched_ctrl->tci_beam_switch); harq->start_tci_timer = false; } - handle_dl_harq(nrmac, UE, pid, success, nrmac->dl_bler.harq_round_max); + handle_dl_harq(nrmac, UE, pid, ack_mask, nrmac->dl_bler.harq_round_max); harq_bit += n_ack_bits; } free(uci_234->harq.harq_payload); @@ -1137,6 +1192,15 @@ static void set_pucch0_vrb_occupation(const NR_sched_pucch_t *pucch, uint16_t *v } } +static void clear_pucch0_vrb_occupation(const NR_sched_pucch_t *pucch, uint16_t *vrb_map_UL, const int bwp_start) +{ + for (int l = 0; l < pucch->nr_of_symb; l++) { + const uint16_t symb = SL_to_bitmap(pucch->start_symb + l, 1); + const int prb = l == 1 && pucch->second_hop_prb != 0 ? pucch->second_hop_prb : pucch->prb_start; + vrb_map_UL[bwp_start + prb] &= ~symb; + } +} + bool check_bits_vs_coderate_limit(NR_PUCCH_Config_t *pucch_Config, int O_uci, int pucch_resource) { int resource_id = get_pucch_resourceid(pucch_Config, O_uci, pucch_resource); @@ -1182,6 +1246,84 @@ bool check_bits_vs_coderate_limit(NR_PUCCH_Config_t *pucch_Config, int O_uci, in AssertFatal(false, "No PUCCH resource found\n"); } +/* PUCCH resource set 0 is limited to two UCI bits. A rank-8 PDSCH consumes + * two HARQ-ACK bits by itself, so multiplexing a second assignment changes + * the selected resource to set 1 (format 2). Keep the scheduler's VRB map in + * sync with the resource that nr_configure_pucch() and the UE will select from + * the final payload size. Without this migration every feedback occasion + * accepts only one rank-8 assignment and halves the usable DL slots. */ +static bool migrate_pucch_to_large_payload_resource(gNB_MAC_INST *mac, + NR_UE_info_t *UE, + NR_sched_pucch_t *pucch, + int old_uci_bits, + int new_uci_bits, + int ue_beam) +{ + if (old_uci_bits > 2 || new_uci_bits <= 2 || pucch->r_pucch >= 0) + return true; + + NR_PUCCH_Config_t *pucch_config = UE->current_UL_BWP.pucch_Config; + const int resource_id = get_pucch_resourceid(pucch_config, new_uci_bits, pucch->resource_indicator); + const NR_PUCCH_Resource_t *resource = NULL; + for (int i = 0; i < pucch_config->resourceToAddModList->list.count; i++) { + const NR_PUCCH_Resource_t *candidate = pucch_config->resourceToAddModList->list.array[i]; + if (candidate->pucch_ResourceId == resource_id) { + resource = candidate; + break; + } + } + if (resource == NULL || resource->format.present != NR_PUCCH_Resource__format_PR_format2 + || resource->intraSlotFrequencyHopping != NULL || resource->secondHopPRB != NULL) { + LOG_W(NR_MAC, + "UE %04x cannot migrate %d-bit HARQ feedback to the configured non-hopping PUCCH format-2 resource\n", + UE->rnti, + new_uci_bits); + return false; + } + + const int n_slots_frame = mac->frame_structure.numb_slots_frame; + NR_beam_alloc_t beam = + beam_allocation_procedure(&mac->beam_info, pucch->frame, pucch->ul_slot, ue_beam, n_slots_frame); + if (beam.idx < 0) + return false; + const int index = ul_buffer_index(pucch->frame, pucch->ul_slot, n_slots_frame, mac->vrb_map_UL_size); + const int bwp_start = UE->current_UL_BWP.BWPStart; + uint16_t *vrb_map_UL = &mac->common_channels[0].vrb_map_UL[beam.idx][index * MAX_BWP_SIZE]; + + /* Release the format-0 reservation before testing format 2 so overlapping + * resource layouts do not conflict with themselves. Restore it if the + * larger footprint is occupied by another UL allocation. */ + clear_pucch0_vrb_occupation(pucch, vrb_map_UL, bwp_start); + const int start_prb = resource->startingPRB; + const int n_prbs = resource->format.choice.format2->nrofPRBs; + const int start_symbol = resource->format.choice.format2->startingSymbolIndex; + const int n_symbols = resource->format.choice.format2->nrofSymbols; + const uint16_t symbol_mask = SL_to_bitmap(start_symbol, n_symbols); + bool available = true; + for (int prb = start_prb; prb < start_prb + n_prbs; prb++) { + if ((vrb_map_UL[bwp_start + prb] & symbol_mask) != 0) { + available = false; + break; + } + } + if (!available) { + set_pucch0_vrb_occupation(pucch, vrb_map_UL, bwp_start); + return false; + } + for (int prb = start_prb; prb < start_prb + n_prbs; prb++) + vrb_map_UL[bwp_start + prb] |= symbol_mask; + + pucch->prb_start = start_prb; + pucch->second_hop_prb = 0; + pucch->nr_of_symb = n_symbols; + pucch->start_symb = start_symbol; + LOG_D(NR_MAC, + "UE %04x migrated HARQ feedback from PUCCH format 0 to format 2 for %d UCI bits\n", + UE->rnti, + new_uci_bits); + return true; +} + // this function returns an index to NR_sched_pucch structure // if the function returns -1 it was not possible to schedule acknack int nr_acknack_scheduling(gNB_MAC_INST *mac, @@ -1203,6 +1345,7 @@ int nr_acknack_scheduling(gNB_MAC_INST *mac, const NR_UE_UL_BWP_t *ul_bwp = &UE->current_UL_BWP; const frame_structure_t *fs = &mac->frame_structure; const int n_slots_frame = fs->numb_slots_frame; + const uint8_t n_ack_bits = harq_ack_bits_per_assignment(UE, is_common); NR_UE_sched_ctrl_t *sched_ctrl = &UE->UE_sched_ctrl; NR_PUCCH_Config_t *pucch_Config = ul_bwp->pucch_Config; @@ -1238,11 +1381,28 @@ int nr_acknack_scheduling(gNB_MAC_INST *mac, if (curr_pucch->active && curr_pucch->frame == pucch_frame && curr_pucch->ul_slot == pucch_slot) { // if there is already a PUCCH in given frame and slot - LOG_D(NR_MAC, "pucch_acknack DL %4d.%2d, UL_ACK %4d.%2d Bits already in current PUCCH: DAI_C %d CSI %d\n", - frame, slot, pucch_frame, pucch_slot, curr_pucch->dai_c, curr_pucch->csi_bits); - // we can't schedule if short pucch is already full - if (curr_pucch->csi_bits == 0 && - curr_pucch->dai_c == 2) + LOG_D(NR_MAC, "pucch_acknack DL %4d.%2d, UL_ACK %4d.%2d Bits already in current PUCCH: DAI_C %d ACK %d CSI %d\n", + frame, slot, pucch_frame, pucch_slot, curr_pucch->dai_c, curr_pucch->harq_ack_bits, curr_pucch->csi_bits); + const int old_uci_bits = curr_pucch->csi_bits + curr_pucch->harq_ack_bits + curr_pucch->sr_flag; + const int new_uci_bits = old_uci_bits + n_ack_bits; + /* Preserve the proven two-PDSCH processing budget per feedback + * occasion. Rank-8 needs format 2 because those two assignments + * produce four HARQ-ACK bits, but accepting more assignments here + * can leave the UE insufficient time to decode every transport block + * before their common feedback deadline. The scheduler will try the + * next configured feedback occasion for additional assignments. */ + const int max_pdsch_assignments_per_feedback = 2; + if (curr_pucch->dai_c >= max_pdsch_assignments_per_feedback) + continue; + if (new_uci_bits > 2 + && !check_bits_vs_coderate_limit(pucch_Config, new_uci_bits, curr_pucch->resource_indicator)) + continue; + if (!migrate_pucch_to_large_payload_resource(mac, + UE, + curr_pucch, + old_uci_bits, + new_uci_bits, + ue_beam)) continue; // if there is CSI but simultaneous HARQ+CSI is disable we can't schedule if (curr_pucch->csi_bits > 0 && !curr_pucch->simultaneous_harqcsi) @@ -1252,21 +1412,17 @@ int nr_acknack_scheduling(gNB_MAC_INST *mac, // the number of bits in the check need to include possible SR (1 bit) // and the ack/nack bit to be scheduled (1 bit) // so the number of bits already scheduled in current pucch + 2 - if (curr_pucch->csi_bits > 0 - && !check_bits_vs_coderate_limit(pucch_Config, - curr_pucch->csi_bits + curr_pucch->dai_c + 2, - curr_pucch->resource_indicator)) - continue; // TODO temporarily limit ack/nak to 3 bits because of performances of polar for PUCCH (required for > 11 bits) - if (curr_pucch->csi_bits > 0 && curr_pucch->dai_c >= 3) + if (curr_pucch->csi_bits > 0 && curr_pucch->harq_ack_bits + n_ack_bits > 3) continue; // otherwise we can schedule in this active PUCCH // no need to check VRB occupation because already done when PUCCH has been activated curr_pucch->timing_indicator = f; curr_pucch->dai_c++; - LOG_D(NR_MAC, "DL %4d.%2d, UL_ACK %4d.%2d Scheduling ACK/NACK in PUCCH %d with timing indicator %d DAI %d CSI %d\n", - frame,slot,curr_pucch->frame,curr_pucch->ul_slot,pucch_index,f,curr_pucch->dai_c,curr_pucch->csi_bits); + curr_pucch->harq_ack_bits += n_ack_bits; + LOG_D(NR_MAC, "DL %4d.%2d, UL_ACK %4d.%2d Scheduling ACK/NACK in PUCCH %d with timing indicator %d DAI %d ACK %d CSI %d\n", + frame,slot,curr_pucch->frame,curr_pucch->ul_slot,pucch_index,f,curr_pucch->dai_c,curr_pucch->harq_ack_bits,curr_pucch->csi_bits); return pucch_index; // index of current PUCCH structure } else if (curr_pucch->active) { @@ -1310,11 +1466,12 @@ int nr_acknack_scheduling(gNB_MAC_INST *mac, curr_pucch->ul_slot = pucch_slot; curr_pucch->timing_indicator = f; // index in the list of timing indicators curr_pucch->dai_c++; + curr_pucch->harq_ack_bits += n_ack_bits; curr_pucch->resource_indicator = 0; // each UE has dedicated PUCCH resources curr_pucch->r_pucch=r_pucch; - LOG_D(NR_MAC, "DL %4d.%2d, UL_ACK %4d.%2d Scheduling ACK/NACK in PUCCH %d with timing indicator %d DAI %d\n", - frame, slot, curr_pucch->frame, curr_pucch->ul_slot, pucch_index, f, curr_pucch->dai_c); + LOG_D(NR_MAC, "DL %4d.%2d, UL_ACK %4d.%2d Scheduling ACK/NACK in PUCCH %d with timing indicator %d DAI %d ACK %d\n", + frame, slot, curr_pucch->frame, curr_pucch->ul_slot, pucch_index, f, curr_pucch->dai_c, curr_pucch->harq_ack_bits); // blocking resources for current PUCCH in VRB map set_pucch0_vrb_occupation(curr_pucch, vrb_map_UL, bwp_start); diff --git a/openair2/LAYER2/NR_MAC_gNB/nr_mac_gNB.h b/openair2/LAYER2/NR_MAC_gNB/nr_mac_gNB.h index 4923309eef..8b5a6cc5a5 100644 --- a/openair2/LAYER2/NR_MAC_gNB/nr_mac_gNB.h +++ b/openair2/LAYER2/NR_MAC_gNB/nr_mac_gNB.h @@ -188,6 +188,8 @@ typedef struct nr_mac_config_s { bool force_UL256qam_off; bool use_deltaMCS; int maxMIMO_layers; + /// PDSCH dmrs-AdditionalPosition: 0 for pos0, 1 for pos1. + int pdsch_dmrs_additional_position; bool disable_harq; nr_power_config_t pusch; /// SNR threshold needed to put or not a PRB in the black list @@ -403,7 +405,9 @@ typedef struct NR_sched_pucch { bool sr_flag; int csi_bits; bool simultaneous_harqcsi; + /// Number of PDSCH assignments represented by the DAI field. uint8_t dai_c; + uint8_t harq_ack_bits; ///< Actual O_ACK payload width (one or two bits per assignment). uint8_t timing_indicator; uint8_t resource_indicator; int r_pucch; @@ -508,6 +512,8 @@ typedef struct NR_sched_pdsch { typedef struct NR_UE_harq { bool is_waiting; uint8_t ndi; + uint8_t ndi2; /* independent NDI for the second transport block */ + uint8_t pending_cw_mask; /* original TBs awaiting ACK; padding CWs are not pending */ uint8_t round; uint16_t feedback_frame; uint16_t feedback_slot; diff --git a/openair2/LAYER2/NR_MAC_gNB/nr_radio_config.c b/openair2/LAYER2/NR_MAC_gNB/nr_radio_config.c index b0ace68241..bab4a6dfc1 100644 --- a/openair2/LAYER2/NR_MAC_gNB/nr_radio_config.c +++ b/openair2/LAYER2/NR_MAC_gNB/nr_radio_config.c @@ -1734,8 +1734,13 @@ static NR_SetupRelease_PDSCH_Config_t *config_pdsch(uint64_t ssb_bitmap, dmrs_DownlinkForPDSCH_MappingTypeA->scramblingID0 = NULL; dmrs_DownlinkForPDSCH_MappingTypeA->scramblingID1 = NULL; dmrs_DownlinkForPDSCH_MappingTypeA->dmrs_AdditionalPosition = calloc(1, sizeof(*dmrs_DownlinkForPDSCH_MappingTypeA->dmrs_AdditionalPosition)); - // TODO possible improvement is to select based on some input additional position - *dmrs_DownlinkForPDSCH_MappingTypeA->dmrs_AdditionalPosition = NR_DMRS_DownlinkConfig__dmrs_AdditionalPosition_pos1; + AssertFatal(configuration->pdsch_dmrs_additional_position == 0 || configuration->pdsch_dmrs_additional_position == 1, + "Invalid PDSCH DMRS additional position %d\n", + configuration->pdsch_dmrs_additional_position); + *dmrs_DownlinkForPDSCH_MappingTypeA->dmrs_AdditionalPosition = + configuration->pdsch_dmrs_additional_position == 0 + ? NR_DMRS_DownlinkConfig__dmrs_AdditionalPosition_pos0 + : NR_DMRS_DownlinkConfig__dmrs_AdditionalPosition_pos1; pdsch_Config->dataScramblingIdentityPDSCH = NULL; pdsch_Config->resourceAllocation = NR_PDSCH_Config__resourceAllocation_resourceAllocationType1; diff --git a/openair2/LAYER2/nr_rlc/nr_rlc_entity.c b/openair2/LAYER2/nr_rlc/nr_rlc_entity.c index 50bf750f6d..86156aa9a0 100644 --- a/openair2/LAYER2/nr_rlc/nr_rlc_entity.c +++ b/openair2/LAYER2/nr_rlc/nr_rlc_entity.c @@ -123,7 +123,7 @@ nr_rlc_entity_t *new_nr_rlc_entity_am(int rx_maxsize, /* let's take average over the last 100 milliseconds * initial_size of 1024 (packets) is arbitrary */ - ret->common.txsdu_avg_time_to_tx = time_average_new(100 * 1000, 1024); + ret->common.txsdu_avg_time_to_tx = time_average_new(1000 * 1000, 1024); ret->rx = nr_rlc_new_rx_manager(1 << (sn_field_length - 1)); @@ -176,7 +176,7 @@ nr_rlc_entity_t *new_nr_rlc_entity_um(int rx_maxsize, /* let's take average over the last 100 milliseconds * initial_size of 1024 is arbitrary */ - ret->common.txsdu_avg_time_to_tx = time_average_new(100 * 1000, 1024); + ret->common.txsdu_avg_time_to_tx = time_average_new(1000 * 1000, 1024); return (nr_rlc_entity_t *)ret; } @@ -215,7 +215,7 @@ nr_rlc_entity_t *new_nr_rlc_entity_tm(int tx_maxsize, /* let's take average over the last 100 milliseconds * initial_size of 1024 is arbitrary */ - ret->common.txsdu_avg_time_to_tx = time_average_new(100 * 1000, 1024); + ret->common.txsdu_avg_time_to_tx = time_average_new(1000 * 1000, 1024); return (nr_rlc_entity_t *)ret; } diff --git a/openair2/LAYER2/nr_rlc/nr_rlc_oai_api.c b/openair2/LAYER2/nr_rlc/nr_rlc_oai_api.c index d0c2afa3cb..080efb47e1 100644 --- a/openair2/LAYER2/nr_rlc/nr_rlc_oai_api.c +++ b/openair2/LAYER2/nr_rlc/nr_rlc_oai_api.c @@ -1023,8 +1023,15 @@ void nr_rlc_activate_avg_time_to_tx(const int ue_id, } if (rb != NULL) { + // Reset the accumulator only on an off->on transition, NOT on every call. + // KPM measurement fills call this once per report period (after reading the + // stats); resetting on every call wiped the sliding-window average before it + // could accumulate, forcing a fallback to the noisy instantaneous HOL + // snapshot. The window self-expires old samples, so no periodic reset is + // needed -- activate once and let it accumulate continuously. + if (is_on && !rb->avg_time_is_on) + time_average_reset(rb->txsdu_avg_time_to_tx); rb->avg_time_is_on = is_on; - time_average_reset(rb->txsdu_avg_time_to_tx); } else { LOG_E(RLC, "Radio Bearer (channel ID %d) is NULL for UE %d\n", channel_id, ue_id); } diff --git a/openair2/RRC/NR/rrc_gNB.c b/openair2/RRC/NR/rrc_gNB.c index 8d1bbf2b8a..019c6abff1 100644 --- a/openair2/RRC/NR/rrc_gNB.c +++ b/openair2/RRC/NR/rrc_gNB.c @@ -84,6 +84,10 @@ #ifdef E2_AGENT #include "openair2/E2AP/RAN_FUNCTION/O-RAN/ran_func_rc_extern.h" +/* PHY simulators link RRC with E2 enabled, but do not link the RC service. + * Keep RC's real mutex when present and avoid a hard simulator dependency. */ +extern void rc_teardown_lock(void) __attribute__((weak)); +extern void rc_teardown_unlock(void) __attribute__((weak)); #define E2_AGENT_SIGNAL_DL_DCCH_RRC_MSG(BUF, LEN, ID) \ do { \ byte_array_t buffer_ba = {.len = LEN}; \ @@ -2735,11 +2739,14 @@ void rrc_remove_ue(gNB_RRC_INST *rrc, rrc_gNB_ue_context_t *ue_context_p) LOG_I(NR_RRC, "removed UE CU UE ID %u/RNTI %04x \n", UE->rrc_ue_id, UE->rnti); rrc_delete_ue_data(UE); #ifdef E2_AGENT - rc_teardown_lock(); /* block in-flight RC indications during teardown */ + const bool rc_lock_available = rc_teardown_lock != NULL && rc_teardown_unlock != NULL; + if (rc_lock_available) + rc_teardown_lock(); /* block in-flight RC indications during teardown */ #endif rrc_gNB_remove_ue_context(rrc, ue_context_p); #ifdef E2_AGENT - rc_teardown_unlock(); + if (rc_lock_available) + rc_teardown_unlock(); #endif } diff --git a/radio/rfsimulator/apply_channelmod.c b/radio/rfsimulator/apply_channelmod.c index 0994231376..3eafdd0e86 100644 --- a/radio/rfsimulator/apply_channelmod.c +++ b/radio/rfsimulator/apply_channelmod.c @@ -227,6 +227,28 @@ void rxAddInput(c16_t **input_sig, cf_t *after_channel_sig, int rxAnt, channel_d double Doppler_phase_cur = channelDesc->Doppler_phase_cur[rxAnt]; Doppler_phase_cur -= 2 * M_PI * round(Doppler_phase_cur / (2 * M_PI)); + /* random_channel() defines AWGN as a one-tap identity matrix. The generic + * convolution below nevertheless scans every TX branch for every RX sample, + * turning an 8x8 RFsim link into 64 complex multiplies per sample. Select + * the sole non-zero branch directly. This is the exact same channel model; + * it only removes multiplication by the known zeros. */ + if (channelDesc->modelid == AWGN && channelDesc->channel_length == 1 + && channelDesc->Doppler_phase_inc == 0.0) { + const int txAnt = rxAnt % nbTx; + const struct complexd channel = channelDesc->ch[rxAnt + (txAnt * channelDesc->nb_rx)][0]; + const bool noise_rounds_to_zero = noise_per_sample < 1e-6; + for (int i = 0; i < nbSamples; i++) { + const struct complex16 tx = input_sig[txAnt][i]; + const double rx_r = tx.r * channel.r - tx.i * channel.i; + const double rx_i = tx.i * channel.r + tx.r * channel.i; + after_channel_sig[i].r += rx_r * pathLossLinear + + (noise_rounds_to_zero ? 0.0 : noise_per_sample * gaussZiggurat(0.0, 1.0)); + after_channel_sig[i].i += rx_i * pathLossLinear + + (noise_rounds_to_zero ? 0.0 : noise_per_sample * gaussZiggurat(0.0, 1.0)); + } + return; + } + for (int i = 0; i < nbSamples; i++) { cf_t *out_ptr = after_channel_sig + i; struct complexd rx_tmp = {0}; diff --git a/radio/rfsimulator/simulator.cpp b/radio/rfsimulator/simulator.cpp index 901c00864f..d391356b40 100644 --- a/radio/rfsimulator/simulator.cpp +++ b/radio/rfsimulator/simulator.cpp @@ -56,6 +56,14 @@ extern int get_currentchannels_type(const char *buf, #define SEND_BUFF_SIZE 100000000 // Socket buffer size #define MAX_BEAMS 64 +// The RF simulator normally transports c16_t samples verbatim. At 106 PRB, +// eight TX ports exceed a 10-Gbit/s link before TCP overhead. IQ8 is an +// explicitly enabled wire encoding: a block is packed only when every I/Q +// component is exactly representable as int8_t, otherwise that block is sent +// in the legacy c16_t format. The magic value avoids confusing this with any +// future use of the otherwise-unused option fields in samplesBlockHeader_t. +static constexpr uint32_t RFSIM_OPTION_IQ8 = 0x49513831U; // "IQ81" + // Simulator role typedef enum { SIMU_ROLE_SERVER = 1, SIMU_ROLE_CLIENT } simuRole; @@ -81,7 +89,8 @@ typedef enum { SIMU_ROLE_SERVER = 1, SIMU_ROLE_CLIENT } simuRole; #define RFSIM_CONFIG_HELP_OPTIONS \ " list of comma separated options to enable rf simulator functionalities. Available options: \n" \ " chanmod: enable channel modelisation\n" \ - " saviq: enable saving written iqs to a file\n" + " saviq: enable saving written iqs to a file\n" \ + " iq8: losslessly pack int8-representable IQ blocks on the wire\n" #define simOpt PARAMFLAG_NOFREE | PARAMFLAG_CMDLINE_NOPREFIXENABLED #define simBool PARAMFLAG_BOOL | PARAMFLAG_NOFREE | PARAMFLAG_CMDLINE_NOPREFIXENABLED @@ -161,6 +170,7 @@ typedef struct buffer_s { rfsim_packet_t *packet_ptr; size_t payload_sz; size_t remainToTransferBeam; + bool packet_iq8; std::queue received_packets; } buffer_t; @@ -191,6 +201,12 @@ typedef struct { double rx_freq; double tx_bw; int channelmod; + bool iq8; + bool iq8_tx_logged; + bool iq8_fallback_logged; + bool iq8_rx_logged; + uint64_t iq8_tx_blocks; + uint64_t iq8_raw_blocks; double chan_pathloss; double chan_forgetfact; uint64_t chan_offset; @@ -463,6 +479,49 @@ static void fullwrite(int fd, void *_buf, ssize_t count, rfsimulator_state_t *t) } } +static bool pack_iq8_block(void ***samplesVoid, + const std::vector &beam_indices, + int nbAnt, + int nsamps, + std::vector &packed, + int *max_abs) +{ + const size_t complex_samples = (size_t)beam_indices.size() * nbAnt * nsamps; + packed.resize(complex_samples * 2); + size_t out = 0; + *max_abs = 0; + for (size_t beam : beam_indices) { + for (int ant = 0; ant < nbAnt; ant++) { + const sample_t *in = static_cast(samplesVoid[beam][ant]); + for (int sample = 0; sample < nsamps; sample++) { + const int real = in[sample].r; + const int imag = in[sample].i; + *max_abs = std::max(*max_abs, std::max(std::abs(real), std::abs(imag))); + if (real < INT8_MIN || real > INT8_MAX || imag < INT8_MIN || imag > INT8_MAX) { + packed.clear(); + return false; + } + packed[out++] = static_cast(real); + packed[out++] = static_cast(imag); + } + } + } + return true; +} + +static void unpack_iq8_block_in_place(char *payload, size_t complex_samples) +{ + int8_t *packed = reinterpret_cast(payload); + sample_t *unpacked = reinterpret_cast(payload); + // Expanding from the end keeps unread packed bytes intact. + for (size_t sample = complex_samples; sample-- > 0;) { + const int16_t real = packed[2 * sample]; + const int16_t imag = packed[2 * sample + 1]; + unpacked[sample].r = real; + unpacked[sample].i = imag; + } +} + static float get_rx_gain_db(rfsimulator_state_t *rfsimulator, uint rx_beam, uint tx_beam) { if (!rfsimulator->beam_ctrl->enable_beams) { @@ -584,6 +643,10 @@ static void rfsimulator_readconfig(rfsimulator_state_t *rfsimulator) rfsimulator->rx_freq, rfsimulator->tx_bw); rfsimulator->channelmod = true; + } else if (strcmp(rfsimuParam[p].strlistptr[i], "iq8") == 0) { + rfsimulator->iq8 = true; + LOG_I(HW, + "RFsim IQ8 wire packing enabled (exact int8 blocks, automatic c16 fallback)\n"); } else { fprintf(stderr, "unknown rfsimulator option: %s\n", rfsimuParam[p].strlistptr[i]); exit(-1); @@ -975,23 +1038,58 @@ static int rfsimulator_write_internal(rfsimulator_state_t *t, mutexlock(t->Sockmutex); LOG_D(HW, "Sending %d samples at time: %ld, nbAnt %d\n", nsamps, timestamp, nbAnt); + const int num_beams = tx_beams.size(); + AssertFatal(num_beams > 0, "Must set at least one bit in beam_map\n"); + // Send beams in order of beam index. This is required for beam_map to work correctly on the receiver side. + std::vector indices(tx_beams.size()); + std::iota(indices.begin(), indices.end(), 0); + std::sort(indices.begin(), indices.end(), [&](size_t i, size_t j) { return tx_beams[i] < tx_beams[j]; }); + + static thread_local std::vector iq8_payload; + int max_abs = 0; + const bool use_iq8 = t->iq8 && pack_iq8_block(samplesVoid, indices, nbAnt, nsamps, iq8_payload, &max_abs); + if (use_iq8) { + t->iq8_tx_blocks++; + if (!t->iq8_tx_logged) { + LOG_I(HW, + "RFsim IQ8 first packed TX block: samples=%d antennas=%d beams=%d max_abs=%d wire=%zu/raw=%zu bytes\n", + nsamps, + nbAnt, + num_beams, + max_abs, + iq8_payload.size(), + sampleToByte(nsamps, nbAnt) * (size_t)num_beams); + t->iq8_tx_logged = true; + } + } else if (t->iq8) { + t->iq8_raw_blocks++; + if (!t->iq8_fallback_logged) { + LOG_W(HW, + "RFsim IQ8 raw fallback: an IQ component is outside [-128,127] (observed_abs=%d)\n", + max_abs); + t->iq8_fallback_logged = true; + } + } + for (int i = 0; i < MAX_FD_RFSIMU; i++) { buffer_t *b = &t->buf[i]; if (b->conn_sock >= 0) { - samplesBlockHeader_t header = {(uint32_t)nsamps, (uint32_t)nbAnt, (uint64_t)timestamp, 0, 0, beams_to_beam_map(tx_beams)}; + samplesBlockHeader_t header = {(uint32_t)nsamps, + (uint32_t)nbAnt, + (uint64_t)timestamp, + 0, + use_iq8 ? RFSIM_OPTION_IQ8 : 0, + beams_to_beam_map(tx_beams)}; fullwrite(b->conn_sock, &header, sizeof(header), t); - int num_beams = tx_beams.size(); - // Send beams in order of beam index. This is required for beam_map to work correctly on the receiver side. - std::vector indices(tx_beams.size()); - std::iota(indices.begin(), indices.end(), 0); - std::sort(indices.begin(), indices.end(), [&](size_t i, size_t j) { return tx_beams[i] < tx_beams[j]; }); - - AssertFatal(num_beams > 0, "Must set at least one bit in beam_map\n"); - for (int beam = 0; beam < num_beams; beam++) { - for (int a = 0; a < nbAnt; a++) { - sample_t *in = (sample_t *)samplesVoid[indices[beam]][a]; - fullwrite(b->conn_sock, (void *)in, sampleToByte(nsamps, 1), t); + if (use_iq8) { + fullwrite(b->conn_sock, iq8_payload.data(), iq8_payload.size(), t); + } else { + for (int beam = 0; beam < num_beams; beam++) { + for (int a = 0; a < nbAnt; a++) { + sample_t *in = (sample_t *)samplesVoid[indices[beam]][a]; + fullwrite(b->conn_sock, (void *)in, sampleToByte(nsamps, 1), t); + } } } } @@ -1106,6 +1204,9 @@ static void process_recv_header(rfsimulator_state_t *t, buffer_t *b, bool first_ { b->headerMode = false; // We got the header AssertFatal(b->th.nbAnt != 0, "Number of antennas not set\n"); + AssertFatal(b->th.option_flag == 0 || b->th.option_flag == RFSIM_OPTION_IQ8, + "Unsupported RFsim sample encoding flag 0x%08x\n", + b->th.option_flag); if (b->nbAnt != b->th.nbAnt) { LOG_A(HW, "RFsim: Number of antennas changed from %d to %d\n", b->nbAnt, b->th.nbAnt); b->nbAnt = b->th.nbAnt; @@ -1132,7 +1233,9 @@ static void process_recv_header(rfsimulator_state_t *t, buffer_t *b, bool first_ b->packet_ptr = static_cast(calloc_or_fail(1, payload_sz + sizeof(samplesBlockHeader_t))); b->packet_ptr->header = b->th; b->transferPtr = b->packet_ptr->payload; - b->remainToTransfer = payload_sz; + b->payload_sz = payload_sz; + b->packet_iq8 = b->th.option_flag == RFSIM_OPTION_IQ8; + b->remainToTransfer = b->packet_iq8 ? payload_sz / 2 : payload_sz; return; } @@ -1274,6 +1377,17 @@ static bool flushInput(rfsimulator_state_t *t, int timeout, bool first_time) b->remainToTransfer = sizeof(samplesBlockHeader_t); if (!b->trashingPacket) { + if (b->packet_iq8) { + const size_t complex_samples = b->payload_sz / sizeof(sample_t); + unpack_iq8_block_in_place(b->packet_ptr->payload, complex_samples); + if (!t->iq8_rx_logged) { + LOG_I(HW, + "RFsim IQ8 first received block: wire=%zu/raw=%zu bytes, restored exactly to c16\n", + b->payload_sz / 2, + b->payload_sz); + t->iq8_rx_logged = true; + } + } b->lastReceivedTS = b->th.timestamp + b->th.size; LOG_D(HW, "UEsock: %d Set b->lastReceivedTS %ld\n", b->conn_sock, b->lastReceivedTS); b->received_packets.emplace(b->packet_ptr); @@ -1281,6 +1395,7 @@ static bool flushInput(rfsimulator_state_t *t, int timeout, bool first_time) free(b->packet_ptr); } b->packet_ptr = NULL; + b->packet_iq8 = false; b->trashingPacket = false; } } diff --git a/targets/PROJECTS/GENERIC-NR-5GC/CONF/uecap_ports1.xml b/targets/PROJECTS/GENERIC-NR-5GC/CONF/uecap_ports1.xml index 513deb33d4..622c88e40c 100644 --- a/targets/PROJECTS/GENERIC-NR-5GC/CONF/uecap_ports1.xml +++ b/targets/PROJECTS/GENERIC-NR-5GC/CONF/uecap_ports1.xml @@ -369,7 +369,7 @@ - + @@ -378,7 +378,7 @@ - + @@ -394,7 +394,7 @@ - + @@ -402,7 +402,7 @@ - + @@ -410,7 +410,7 @@ - + @@ -418,7 +418,7 @@ - + @@ -434,7 +434,7 @@ - + @@ -450,7 +450,7 @@ - + @@ -458,7 +458,7 @@ - +