diff --git a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_dlsch.c b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_dlsch.c index 5cdc1e4d4f..cb6c72cfdb 100644 --- a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_dlsch.c +++ b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_dlsch.c @@ -779,6 +779,11 @@ static void pf_dl(gNB_MAC_INST *mac, qsort(UE_sched, numUE, sizeof(UEsched_t), comparator); UEsched_t *iterator = UE_sched; + /* nakaolab: tag to mark UEs already paired in this pf_dl call. Increments each + * call so a UE's stored tag != current tag means "available for pairing". */ + static uint64_t mumimo_pf_call_id = 0; + if (mumimo_enabled()) + mumimo_pf_call_id++; /* nakaolab: diagnostic — log numUE > 1 to understand MU-MIMO opportunity */ if (mumimo_enabled() && numUE >= 2) { static int dump_count = 0; @@ -791,8 +796,10 @@ static void pf_dl(gNB_MAC_INST *mac, /* Loop UE_sched to find max coeff and allocate transmission */ while (iterator->UE != NULL) { - /* nakaolab: skip UEs already allocated as a MU-MIMO partner of an earlier UE */ - if (iterator->mumimo_paired) { + /* nakaolab: skip UEs already allocated as a MU-MIMO partner of an earlier UE + * in this same pf_dl call (mumimo_pf_call_id matches). */ + if (mumimo_enabled() && + iterator->UE->UE_sched_ctrl.mumimo_paired_slot == (int64_t)mumimo_pf_call_id) { iterator++; continue; } @@ -942,9 +949,8 @@ static void pf_dl(gNB_MAC_INST *mac, /* nakaolab MU-MIMO: try to find an orthogonal-PMI partner UE that can ride on * the same RB allocation as this UE. The partner is given a separate DCI with * its own CCE, and a different DMRS CDM group so the receiver can separate - * the two layers. Both UEs are forced to the same MCS/TBS (we trust the leader - * UE's PF coefficient and channel report). */ - UEsched_t *partner = NULL; + * the two layers. Both UEs are forced to the same MCS/TBS. */ + NR_UE_info_t *partner = NULL; int partner_CCE = -1; int partner_alloc = -1; @@ -952,83 +958,99 @@ static void pf_dl(gNB_MAC_INST *mac, /* For MU-MIMO, the leader UE's DMRS group must be 2 so the second group is * available for the partner. Patch the just-built sched_pdsch in place. */ static int diag_count = 0; - bool log_diag = (diag_count++ < 20); + /* Log every Nth call to avoid log flood, but never stop logging. */ + bool log_diag = ((diag_count++ % 200) == 0); if (log_diag) { int n_cand = 0; - for (UEsched_t *c = UE_sched; c->UE != NULL; c++) - if (c != iterator && !c->mumimo_paired) n_cand++; - LOG_W(NR_MAC, "[mumimo-diag] %4d.%2d ENTER partner-search after RNTI %04x; eligible_others=%d\n", + UE_iterator (UE_list, c) { + if (c != iterator->UE && nr_mac_ue_is_active(c) && + c->UE_sched_ctrl.available_dl_harq.head >= 0) + n_cand++; + } + LOG_W(NR_MAC, "[mumimo-diag] %4d.%2d ENTER partner-search after RNTI %04x; eligible_others=%d (from connected_ue_list)\n", frame, slot, iterator->UE->rnti, n_cand); } - /* Scan ALL entries in UE_sched (not just iterator+1) — partner can be at any - * position. The leader's PF order doesn't constrain orthogonality. */ - for (UEsched_t *cand = UE_sched; cand->UE != NULL; cand++) { - if (cand == iterator) + /* nakaolab Phase 3: scan ALL connected UEs (not just UE_sched). PF often has + * just one UE per slot in UE_sched even with many UEs sending traffic; the + * MU-MIMO partner can be any orthogonal UE that has buffered data. */ + UE_iterator (UE_list, cand_ue) { + if (cand_ue == iterator->UE) continue; - if (cand->mumimo_paired) { - if (log_diag) LOG_W(NR_MAC, "[mumimo-diag] %4d.%2d skip RNTI %04x: already paired\n", frame, slot, cand->UE->rnti); + if (cand_ue->UE_sched_ctrl.mumimo_paired_slot == (int64_t)mumimo_pf_call_id) { + if (log_diag) LOG_W(NR_MAC, "[mumimo-diag] %4d.%2d skip RNTI %04x: already paired this slot\n", frame, slot, cand_ue->rnti); continue; } - if (cand->UE->UE_sched_ctrl.available_dl_harq.head < 0) { - if (log_diag) LOG_W(NR_MAC, "[mumimo-diag] %4d.%2d skip RNTI %04x: no HARQ\n", frame, slot, cand->UE->rnti); + if (!nr_mac_ue_is_active(cand_ue)) { + if (log_diag) LOG_W(NR_MAC, "[mumimo-diag] %4d.%2d skip RNTI %04x: inactive\n", frame, slot, cand_ue->rnti); continue; } - if (!pmi_is_orthogonal(iterator->UE, cand->UE)) { - if (log_diag) LOG_W(NR_MAC, "[mumimo-diag] %4d.%2d skip RNTI %04x: PMI not orthogonal (leader ri=%d, cand ri=%d)\n", - frame, slot, cand->UE->rnti, - iterator->UE->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.ri, - cand->UE->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.ri); + if (cand_ue->UE_sched_ctrl.available_dl_harq.head < 0) { + if (log_diag) LOG_W(NR_MAC, "[mumimo-diag] %4d.%2d skip RNTI %04x: no HARQ\n", frame, slot, cand_ue->rnti); continue; } - /* Candidate's beam must match ours so the precoding is on the same TRX - * resources. (Different beams would also work but our config has a - * single beam pool.) */ + /* Pull RLC bytes into the partner's buffer; if no data, skip. */ + update_dlsch_buffer(pp_pdsch->frame, pp_pdsch->slot, cand_ue); + if (!dlsch_to_schedule(&cand_ue->UE_sched_ctrl)) { + if (log_diag) LOG_W(NR_MAC, "[mumimo-diag] %4d.%2d skip RNTI %04x: no data\n", frame, slot, cand_ue->rnti); + continue; + } + if (!pmi_is_orthogonal(iterator->UE, cand_ue)) { + if (log_diag) LOG_W(NR_MAC, "[mumimo-diag] %4d.%2d skip RNTI %04x: PMI not orthogonal (leader x1=%d x2=%d, cand x1=%d x2=%d)\n", + frame, slot, cand_ue->rnti, + iterator->UE->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.pmi_x1, + iterator->UE->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.pmi_x2, + cand_ue->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.pmi_x1, + cand_ue->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.pmi_x2); + continue; + } + /* Candidate's beam must match ours. */ NR_beam_alloc_t pbeam = beam_allocation_procedure(&mac->beam_info, frame, slot, - cand->UE->UE_beam_index, + cand_ue->UE_beam_index, slots_per_frame); if (pbeam.idx != beam.idx) { - if (log_diag) LOG_W(NR_MAC, "[mumimo-diag] %4d.%2d skip RNTI %04x: beam idx %d != leader %d\n", frame, slot, cand->UE->rnti, pbeam.idx, beam.idx); - reset_beam_status(&mac->beam_info, frame, slot, cand->UE->UE_beam_index, + if (log_diag) LOG_W(NR_MAC, "[mumimo-diag] %4d.%2d skip RNTI %04x: beam %d != leader %d\n", frame, slot, cand_ue->rnti, pbeam.idx, beam.idx); + reset_beam_status(&mac->beam_info, frame, slot, cand_ue->UE_beam_index, slots_per_frame, pbeam.new_beam); continue; } /* Need a free CCE for the partner's own DCI. */ - partner_CCE = get_cce_index(mac, CC_id, slot, cand->UE->rnti, - &cand->UE->UE_sched_ctrl.aggregation_level, + partner_CCE = get_cce_index(mac, CC_id, slot, cand_ue->rnti, + &cand_ue->UE_sched_ctrl.aggregation_level, pbeam.idx, - cand->UE->UE_sched_ctrl.search_space, - cand->UE->UE_sched_ctrl.coreset, - &cand->UE->UE_sched_ctrl.sched_pdcch, - cand->UE->UE_sched_ctrl.pdcch_cl_adjust); + cand_ue->UE_sched_ctrl.search_space, + cand_ue->UE_sched_ctrl.coreset, + &cand_ue->UE_sched_ctrl.sched_pdcch, + cand_ue->UE_sched_ctrl.pdcch_cl_adjust); if (partner_CCE < 0) { - if (log_diag) LOG_W(NR_MAC, "[mumimo-diag] %4d.%2d skip RNTI %04x: no free CCE\n", frame, slot, cand->UE->rnti); - reset_beam_status(&mac->beam_info, frame, slot, cand->UE->UE_beam_index, + if (log_diag) LOG_W(NR_MAC, "[mumimo-diag] %4d.%2d skip RNTI %04x: no free CCE\n", frame, slot, cand_ue->rnti); + reset_beam_status(&mac->beam_info, frame, slot, cand_ue->UE_beam_index, slots_per_frame, pbeam.new_beam); continue; } /* And a PUCCH for HARQ feedback. */ - if (!get_FeedbackDisabled(cand->UE->sc_info.downlinkHARQ_FeedbackDisabled_r17, - cand->UE->UE_sched_ctrl.available_dl_harq.head)) { - int p_pucch = nr_get_pucch_resource(cand->UE->UE_sched_ctrl.coreset, - cand->UE->current_UL_BWP.pucch_Config, + if (!get_FeedbackDisabled(cand_ue->sc_info.downlinkHARQ_FeedbackDisabled_r17, + cand_ue->UE_sched_ctrl.available_dl_harq.head)) { + int p_pucch = nr_get_pucch_resource(cand_ue->UE_sched_ctrl.coreset, + cand_ue->current_UL_BWP.pucch_Config, partner_CCE); - partner_alloc = nr_acknack_scheduling(mac, cand->UE, frame, slot, - cand->UE->UE_beam_index, p_pucch, 0); + partner_alloc = nr_acknack_scheduling(mac, cand_ue, frame, slot, + cand_ue->UE_beam_index, p_pucch, 0); if (partner_alloc < 0) { - reset_beam_status(&mac->beam_info, frame, slot, cand->UE->UE_beam_index, + if (log_diag) LOG_W(NR_MAC, "[mumimo-diag] %4d.%2d skip RNTI %04x: no PUCCH\n", frame, slot, cand_ue->rnti); + reset_beam_status(&mac->beam_info, frame, slot, cand_ue->UE_beam_index, slots_per_frame, pbeam.new_beam); continue; } } - partner = cand; - LOG_D(NR_MAC, "[nakaolab MU-MIMO] %4d.%2d pair RNTI %04x (x1=%d x2=%d) + RNTI %04x (x1=%d x2=%d) on RBs [%d..%d]\n", + partner = cand_ue; + LOG_W(NR_MAC, "[nakaolab MU-MIMO] %4d.%2d pair RNTI %04x (x1=%d x2=%d) + RNTI %04x (x1=%d x2=%d) on RBs [%d..%d]\n", frame, slot, iterator->UE->rnti, iterator->UE->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.pmi_x1, iterator->UE->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.pmi_x2, - cand->UE->rnti, - cand->UE->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.pmi_x1, - cand->UE->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.pmi_x2, + cand_ue->rnti, + cand_ue->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.pmi_x1, + cand_ue->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.pmi_x2, sched_pdsch.rbStart, sched_pdsch.rbStart + sched_pdsch.rbSize - 1); break; } @@ -1039,11 +1061,13 @@ static void pf_dl(gNB_MAC_INST *mac, } post_process_dlsch(mac, pp_pdsch, iterator->UE, &sched_pdsch); + if (mumimo_enabled()) + iterator->UE->UE_sched_ctrl.mumimo_paired_slot = (int64_t)mumimo_pf_call_id; /* nakaolab: now allocate the MU-MIMO partner on the same RBs. */ if (partner != NULL) { - NR_UE_sched_ctrl_t *p_sched_ctrl = &partner->UE->UE_sched_ctrl; - NR_UE_DL_BWP_t *p_dl_bwp = &partner->UE->current_DL_BWP; + NR_UE_sched_ctrl_t *p_sched_ctrl = &partner->UE_sched_ctrl; + NR_UE_DL_BWP_t *p_dl_bwp = &partner->current_DL_BWP; p_sched_ctrl->cce_index = partner_CCE; fill_pdcch_vrb_map(mac, CC_id, &p_sched_ctrl->sched_pdcch, partner_CCE, @@ -1059,10 +1083,10 @@ static void pf_dl(gNB_MAC_INST *mac, .tb_size = sched_pdsch.tb_size, .dl_harq_pid = p_sched_ctrl->available_dl_harq.head, .pucch_allocation = partner_alloc, - .pm_index = get_pm_index(mac, partner->UE, p_dl_bwp->dci_format, p_l, + .pm_index = get_pm_index(mac, partner, p_dl_bwp->dci_format, p_l, mac->radio_config.pdsch_AntennaPorts.XP), .nrOfLayers = p_l, - .bwp_info = get_pdsch_bwp_start_size(mac, partner->UE), + .bwp_info = get_pdsch_bwp_start_size(mac, partner), .dmrs_parms = get_dl_dmrs_params(scc, p_dl_bwp, &tda_info, p_l), .time_domain_allocation = tda, .tda_info = tda_info, @@ -1074,8 +1098,8 @@ static void pf_dl(gNB_MAC_INST *mac, p_sched_pdsch.dmrs_parms.dmrs_ports_id = 2; p_sched_pdsch.dmrs_parms.numDmrsCdmGrpsNoData = 2; - post_process_dlsch(mac, pp_pdsch, partner->UE, &p_sched_pdsch); - partner->mumimo_paired = true; + post_process_dlsch(mac, pp_pdsch, partner, &p_sched_pdsch); + p_sched_ctrl->mumimo_paired_slot = (int64_t)mumimo_pf_call_id; remainUEs[beam.idx]--; } diff --git a/openair2/LAYER2/NR_MAC_gNB/nr_mac_gNB.h b/openair2/LAYER2/NR_MAC_gNB/nr_mac_gNB.h index 391bfeabe8..3ae9419fd9 100644 --- a/openair2/LAYER2/NR_MAC_gNB/nr_mac_gNB.h +++ b/openair2/LAYER2/NR_MAC_gNB/nr_mac_gNB.h @@ -65,6 +65,7 @@ #include "LAYER2/NR_MAC_COMMON/nr_mac_common.h" #include "LAYER2/NR_MAC_gNB/mac_config.h" #include "NR_TAG.h" +#include "PHY/NR_TRANSPORT/nr_antenna_state.h" /* Defs */ #define MAX_NUM_BWP 5 @@ -663,6 +664,10 @@ typedef struct { int release_timer; CSI_report_t CSI_report; bool SR; + /// nakaolab MU-MIMO: encoded frame*1000+slot when this UE was last allocated as + /// a MU-MIMO partner (or leader). -1 = never. Lets pf_dl skip UEs already paired + /// in the current slot when scanning connected_ue_list for partners. + int64_t mumimo_paired_slot; /// information about every HARQ process NR_UE_harq_t harq_processes[NR_MAX_HARQ_PROCESSES]; /// HARQ processes that are free @@ -965,6 +970,10 @@ typedef struct gNB_MAC_INST_s { nr_mac_config_t radio_config; nr_rlc_configuration_t rlc_config; + /// Pointer to PHY antenna state (port mute mask) — set in scheduler init, + /// used by get_dl_nrOfLayers to cap MIMO rank to active TX port count. + nr_antenna_state_t *antenna_state; + NR_sched_ctrl_sib1_t *sched_ctrlSIB1; NR_sched_pdcch_t *sched_pdcch_otherSI; uint16_t cset0_bwp_start; diff --git a/radio/rfsimulator/simulator.cpp b/radio/rfsimulator/simulator.cpp index 3b2f64dbca..4905172def 100644 --- a/radio/rfsimulator/simulator.cpp +++ b/radio/rfsimulator/simulator.cpp @@ -1324,7 +1324,29 @@ static void rfsimulator_read_internal(rfsimulator_state_t *t, rxAddInput(input, temp_array[aarx], aarx, ptr->channel_model, nsamps); } } else { - if (is_first_beam && is_first_peer && (ptr->nbAnt == 1 || nbAnt == 1)) { + /* nakaolab MU-MIMO: when OAI_UE_CHANNEL_CLASS is set in the env, the UE + * applies a deterministic rank-1 channel whose dominant right-singular-vector + * matches Type I 4-port codebook entry w_class. This makes the UE report a + * stable PMI = class, giving the gNB scheduler natural PMI diversity across + * UEs assigned to different classes. Four classes give four mutually-orthogonal + * codewords, supporting up to 4-way MU-MIMO. + * + * Codeword w_k[a] for 4-port rank-1 (Type I, N1=2, N2=1): + * w_0 = [1, 1, 1, 1]/2 + * w_1 = [1, j, -1, -j]/2 + * w_2 = [1, -1, 1, -1]/2 + * w_3 = [1, -j, -1, j]/2 + * The 4x4 channel H^(k) is rank-1: H^(k)[r,a] = w_k[a]* (per-row constant), so + * the dominant right singular vector is w_k. */ + static int ue_class_cached = -2; + if (ue_class_cached == -2) { + const char *env = getenv("OAI_UE_CHANNEL_CLASS"); + ue_class_cached = (env && atoi(env) >= 0 && atoi(env) < 4) ? atoi(env) : -1; + if (ue_class_cached >= 0) + LOG_W(HW, "[nakaolab MU-MIMO] rfsim DL channel: orthogonal class %d\n", ue_class_cached); + } + + if (is_first_beam && is_first_peer && (ptr->nbAnt == 1 || nbAnt == 1) && ue_class_cached < 0) { // optimization: The buffer is uninitialized so samples can be written directly in the buffer combine_received_beams(t, ptr->received_packets, timestamp - t->chan_offset, 1, nsamps, rx_beam_id, samples); } else { @@ -1334,17 +1356,64 @@ static void rfsimulator_read_internal(rfsimulator_state_t *t, input[aatx] = ant_buffers[aatx].data(); } combine_received_beams(t, ptr->received_packets, timestamp - t->chan_offset, ptr->nbAnt, nsamps, rx_beam_id, input); - for (int aarx = 0; aarx < nbAnt; aarx++) { - double H_awgn_mimo_coeff[ant_buffers.size()]; - for (int aatx = 0; aatx < (int)ant_buffers.size(); aatx++) { - uint32_t ant_diff = std::abs(aatx - aarx); - H_awgn_mimo_coeff[aatx] = ant_diff ? (0.2 / ant_diff) : 1.0; - } - for (uint aatx = 0; aatx < ant_buffers.size(); aatx++) { - for (int i = 0; i < nsamps; i++) { - samples[aarx][i].r += ant_buffers[aatx][i].r * H_awgn_mimo_coeff[aatx]; - samples[aarx][i].i += ant_buffers[aatx][i].i * H_awgn_mimo_coeff[aatx]; + if (ue_class_cached >= 0 && ant_buffers.size() == 4) { + /* nakaolab MU-MIMO: per-UE channel that combines the default symmetric + * rfsim kernel with a class-specific TX-antenna weighting w_k[a]: + * + * H^(k)[r, a] = H_orig[r,a] * w_k[a] + * + * The default kernel H_orig[r,a] = (a==r ? 1.0 : 0.2/|a-r|) is full-rank, + * preserving SSB and reference-signal sync (signal reaches all RX). The + * class-specific weighting rotates each TX antenna's contribution + * differently per class, so the optimal PMI for class k is exactly + * codeword w_k from the Type I 4-port codebook (modulo diagonal scaling). + * + * w_0 = [1, 1, 1, 1]/2 + * w_1 = [1, j, -1, -j]/2 + * w_2 = [1, -1, 1, -1]/2 + * w_3 = [1, -j, -1, j]/2 */ + static const double W_RE[4][4] = { + { 0.5, 0.5, 0.5, 0.5 }, + { 0.5, 0.0, -0.5, 0.0 }, + { 0.5, -0.5, 0.5, -0.5 }, + { 0.5, 0.0, -0.5, 0.0 } + }; + static const double W_IM[4][4] = { + { 0.0, 0.0, 0.0, 0.0 }, + { 0.0, 0.5, 0.0, -0.5 }, + { 0.0, 0.0, 0.0, 0.0 }, + { 0.0, -0.5, 0.0, 0.5 } + }; + const int k = ue_class_cached; + for (int aarx = 0; aarx < nbAnt; aarx++) { + for (int aatx = 0; aatx < (int)ant_buffers.size(); aatx++) { + const uint32_t ant_diff = (uint32_t)std::abs(aatx - aarx); + const double h_orig = ant_diff ? (0.2 / ant_diff) : 1.0; + /* Effective channel coeff: h_orig * w_k[a] (complex multiply). */ + const double hre = h_orig * W_RE[k][aatx]; + const double him = h_orig * W_IM[k][aatx]; + for (int i = 0; i < nsamps; i++) { + const double xr = ant_buffers[aatx][i].r; + const double xi = ant_buffers[aatx][i].i; + samples[aarx][i].r += (int16_t)(hre * xr - him * xi); + samples[aarx][i].i += (int16_t)(hre * xi + him * xr); + } + } + } + } else { + for (int aarx = 0; aarx < nbAnt; aarx++) { + double H_awgn_mimo_coeff[ant_buffers.size()]; + for (int aatx = 0; aatx < (int)ant_buffers.size(); aatx++) { + uint32_t ant_diff = std::abs(aatx - aarx); + H_awgn_mimo_coeff[aatx] = ant_diff ? (0.2 / ant_diff) : 1.0; + } + + for (uint aatx = 0; aatx < ant_buffers.size(); aatx++) { + for (int i = 0; i < nsamps; i++) { + samples[aarx][i].r += ant_buffers[aatx][i].r * H_awgn_mimo_coeff[aatx]; + samples[aarx][i].i += ant_buffers[aatx][i].i * H_awgn_mimo_coeff[aatx]; + } } } }