diff --git a/openair2/GNB_APP/MACRLC_nr_paramdef.h b/openair2/GNB_APP/MACRLC_nr_paramdef.h
index e634f077e269c030b2ab53da28f8f2ff7e216e1e..a7bc3e85454f200f7220c39c8e345475008c1dd0 100644
--- a/openair2/GNB_APP/MACRLC_nr_paramdef.h
+++ b/openair2/GNB_APP/MACRLC_nr_paramdef.h
@@ -62,12 +62,15 @@
 #define CONFIG_STRING_MACRLC_PUSCHFAILURETHRES             "pusch_FailureThres"
 #define CONFIG_STRING_MACRLC_DL_BLER_TARGET_UPPER          "dl_bler_target_upper"
 #define CONFIG_STRING_MACRLC_DL_BLER_TARGET_LOWER          "dl_bler_target_lower"
-#define CONFIG_STRING_MACRLC_DL_RD2_BLER_THRESHOLD         "dl_rd2_bler_threshold"
 #define CONFIG_STRING_MACRLC_DL_MAX_MCS                    "dl_max_mcs"
+#define CONFIG_STRING_MACRLC_UL_BLER_TARGET_UPPER          "ul_bler_target_upper"
+#define CONFIG_STRING_MACRLC_UL_BLER_TARGET_LOWER          "ul_bler_target_lower"
+#define CONFIG_STRING_MACRLC_UL_MAX_MCS                    "ul_max_mcs"
 #define CONFIG_STRING_MACRLC_HARQ_ROUND_MAX                "harq_round_max"
 #define CONFIG_STRING_MACRLC_MIN_GRANT_PRB                 "min_grant_prb"
 #define CONFIG_STRING_MACRLC_MIN_GRANT_MCS                 "min_grant_mcs"
 
+
 /*-------------------------------------------------------------------------------------------------------------------------------------------------------*/
 /*                                            MacRLC  configuration parameters                                                                           */
 /*   optname                                            helpstr   paramflags    XXXptr              defXXXval                  type           numelt     */
@@ -97,8 +100,10 @@
 {CONFIG_STRING_MACRLC_PUSCHFAILURETHRES,                 NULL,     0,          iptr:NULL,           defintval:10,              TYPE_INT,      0},        \
 {CONFIG_STRING_MACRLC_DL_BLER_TARGET_UPPER,   "Upper threshold of BLER to decrease DL MCS",   0, dblptr:NULL,  defdblval:0.15,  TYPE_DOUBLE,  0},        \
 {CONFIG_STRING_MACRLC_DL_BLER_TARGET_LOWER,   "Lower threshold of BLER to increase DL MCS",   0, dblptr:NULL,  defdblval:0.05,  TYPE_DOUBLE,  0},        \
-{CONFIG_STRING_MACRLC_DL_RD2_BLER_THRESHOLD,  "Threshold of RD2/RETX2 BLER to decrease DL MCS", 0, dblptr:NULL,  defdblval:0.01,  TYPE_DOUBLE,  0},      \
 {CONFIG_STRING_MACRLC_DL_MAX_MCS,             "Maximum DL MCS that should be used", 0, u8ptr:NULL,  defintval:28,  TYPE_UINT8,  0},      \
+{CONFIG_STRING_MACRLC_UL_BLER_TARGET_UPPER,   "Upper threshold of BLER to decrease UL MCS",   0, dblptr:NULL,  defdblval:0.15,  TYPE_DOUBLE,  0},        \
+{CONFIG_STRING_MACRLC_UL_BLER_TARGET_LOWER,   "Lower threshold of BLER to increase UL MCS",   0, dblptr:NULL,  defdblval:0.05,  TYPE_DOUBLE,  0},        \
+{CONFIG_STRING_MACRLC_UL_MAX_MCS,             "Maximum UL MCS that should be used", 0, u8ptr:NULL,  defintval:9,   TYPE_UINT8,  0},      \
 {CONFIG_STRING_MACRLC_HARQ_ROUND_MAX,         "Maximum number of HARQ rounds", 0, u8ptr:NULL, defintval:4, TYPE_UINT8, 0}, \
 {CONFIG_STRING_MACRLC_MIN_GRANT_PRB,         "Minimal Periodic ULSCH Grant PRBs", 0, u8ptr:NULL, defintval:5, TYPE_UINT8, 0}, \
 {CONFIG_STRING_MACRLC_MIN_GRANT_MCS,         "Minimal Periodic ULSCH Grant MCS", 0, u8ptr:NULL, defintval:9, TYPE_UINT8, 0} \
@@ -127,10 +132,13 @@
 #define MACRLC_PUSCHFAILURETHRES_IDX                           21
 #define MACRLC_DL_BLER_TARGET_UPPER_IDX                        22
 #define MACRLC_DL_BLER_TARGET_LOWER_IDX                        23
-#define MACRLC_DL_RD2_BLER_THRESHOLD_IDX                       24
-#define MACRLC_DL_MAX_MCS_IDX                                  25
-#define MACRLC_HARQ_ROUND_MAX_IDX                              26
-#define MACRLC_MIN_GRANT_PRB_IDX                               27
-#define MACRLC_MIN_GRANT_MCS_IDX                               28
+#define MACRLC_DL_MAX_MCS_IDX                                  24
+#define MACRLC_UL_BLER_TARGET_UPPER_IDX                        25
+#define MACRLC_UL_BLER_TARGET_LOWER_IDX                        26
+#define MACRLC_UL_MAX_MCS_IDX                                  27
+#define MACRLC_HARQ_ROUND_MAX_IDX                              28
+#define MACRLC_MIN_GRANT_PRB_IDX                               29
+#define MACRLC_MIN_GRANT_MCS_IDX                               30
+
 /*---------------------------------------------------------------------------------------------------------------------------------------------------------*/
 #endif
diff --git a/openair2/GNB_APP/gnb_config.c b/openair2/GNB_APP/gnb_config.c
index c11d6b8092353c065864c083283dc3dcac49df6e..9292d1f96210f883899aed8725af726a5f01dd3f 100644
--- a/openair2/GNB_APP/gnb_config.c
+++ b/openair2/GNB_APP/gnb_config.c
@@ -873,10 +873,14 @@ void RCconfig_nr_macrlc() {
         AssertFatal(1==0,"MACRLC %d: %s unknown southbound midhaul\n",j,*(MacRLC_ParamList.paramarray[j][MACRLC_TRANSPORT_S_PREFERENCE_IDX].strptr));
       } 
       RC.nrmac[j]->ulsch_max_frame_inactivity = *(MacRLC_ParamList.paramarray[j][MACRLC_ULSCH_MAX_FRAME_INACTIVITY].uptr);
-      RC.nrmac[j]->dl_bler_target_upper = *(MacRLC_ParamList.paramarray[j][MACRLC_DL_BLER_TARGET_UPPER_IDX].dblptr);
-      RC.nrmac[j]->dl_bler_target_lower = *(MacRLC_ParamList.paramarray[j][MACRLC_DL_BLER_TARGET_LOWER_IDX].dblptr);
-      RC.nrmac[j]->dl_rd2_bler_threshold = *(MacRLC_ParamList.paramarray[j][MACRLC_DL_RD2_BLER_THRESHOLD_IDX].dblptr);
-      RC.nrmac[j]->dl_max_mcs = *(MacRLC_ParamList.paramarray[j][MACRLC_DL_MAX_MCS_IDX].u8ptr);
+      NR_bler_options_t *dl_bler_options = &RC.nrmac[j]->dl_bler;
+      dl_bler_options->upper = *(MacRLC_ParamList.paramarray[j][MACRLC_DL_BLER_TARGET_UPPER_IDX].dblptr);
+      dl_bler_options->lower = *(MacRLC_ParamList.paramarray[j][MACRLC_DL_BLER_TARGET_LOWER_IDX].dblptr);
+      dl_bler_options->max_mcs = *(MacRLC_ParamList.paramarray[j][MACRLC_DL_MAX_MCS_IDX].u8ptr);
+      NR_bler_options_t *ul_bler_options = &RC.nrmac[j]->ul_bler;
+      ul_bler_options->upper = *(MacRLC_ParamList.paramarray[j][MACRLC_UL_BLER_TARGET_UPPER_IDX].dblptr);
+      ul_bler_options->lower = *(MacRLC_ParamList.paramarray[j][MACRLC_UL_BLER_TARGET_LOWER_IDX].dblptr);
+      ul_bler_options->max_mcs = *(MacRLC_ParamList.paramarray[j][MACRLC_UL_MAX_MCS_IDX].u8ptr);
       RC.nrmac[j]->harq_round_max = *(MacRLC_ParamList.paramarray[j][MACRLC_HARQ_ROUND_MAX_IDX].u8ptr);
       RC.nrmac[j]->min_grant_prb = *(MacRLC_ParamList.paramarray[j][MACRLC_MIN_GRANT_PRB_IDX].u8ptr);
       RC.nrmac[j]->min_grant_mcs = *(MacRLC_ParamList.paramarray[j][MACRLC_MIN_GRANT_MCS_IDX].u8ptr);
diff --git a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_RA.c b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_RA.c
index 20951be0e845c57008cb44efb5c971827aed6728..e95899ecea173addf305c07aa44b153ce0e51c66 100644
--- a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_RA.c
+++ b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_RA.c
@@ -1983,8 +1983,8 @@ void nr_check_Msg4_Ack(module_id_t module_id, int CC_id, frame_t frame, sub_fram
 
   if (harq->is_waiting == 0) {
     if (harq->round == 0) {
-      if (stats->dlsch_errors == 0) {
-        LOG_I(NR_MAC, "(ue %i, rnti 0x%04x) Received Ack of RA-Msg4. CBRA procedure succeeded!\n", UE_id, ra->rnti);
+      if (stats->dl.errors == 0) {
+        LOG_A(NR_MAC, "(ue %i, rnti 0x%04x) Received Ack of RA-Msg4. CBRA procedure succeeded!\n", UE_id, ra->rnti);
         UE_info->active[UE_id] = true;
         UE_info->Msg4_ACKed[UE_id] = true;
 
diff --git a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_dlsch.c b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_dlsch.c
index 221dd0b5cecc058440742dfd8e98ad33935cd3cc..aab77c515a2c291d77593048838ad345b202d1ac 100644
--- a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_dlsch.c
+++ b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_dlsch.c
@@ -397,81 +397,6 @@ int nr_write_ce_dlsch_pdu(module_id_t module_idP,
   return offset;
 }
 
-#define BLER_UPDATE_FRAME 10
-#define BLER_FILTER 0.9f
-int get_mcs_from_bler(module_id_t mod_id, int CC_id, frame_t frame, sub_frame_t slot, int UE_id, int mcs_table) {
-  gNB_MAC_INST *nrmac = RC.nrmac[mod_id];
-  const NR_ServingCellConfigCommon_t *scc = nrmac->common_channels[CC_id].ServingCellConfigCommon;
-  const int n = nr_slots_per_frame[*scc->ssbSubcarrierSpacing];
-  int max_allowed_mcs = (mcs_table == 1) ? 27 : 28;
-  int max_mcs = nrmac->dl_max_mcs;
-
-  if (nrmac->dl_max_mcs>max_allowed_mcs)
-    max_mcs = max_allowed_mcs;
-
-  NR_DL_bler_stats_t *bler_stats = &nrmac->UE_info.UE_sched_ctrl[UE_id].dl_bler_stats;
-
-  /* first call: everything is zero. Initialize to sensible default */
-  if (bler_stats->last_frame_slot == 0 && bler_stats->mcs == 0) {
-    bler_stats->last_frame_slot = frame * n + slot;
-    bler_stats->mcs = 9;
-    bler_stats->bler = (nrmac->dl_bler_target_lower + nrmac->dl_bler_target_upper) / 2;
-    bler_stats->rd2_bler = nrmac->dl_rd2_bler_threshold;
-  }
-
-  const int now = frame * n + slot;
-  int diff = now - bler_stats->last_frame_slot;
-
-  if (diff < 0) // wrap around
-    diff += 1024 * n;
-
-  const uint8_t old_mcs = bler_stats->mcs;
-  const NR_mac_stats_t *stats = &nrmac->UE_info.mac_stats[UE_id];
-
-  // TODO put back this condition when relevant
-  /*const int dret3x = stats->dlsch_rounds[3] - bler_stats->dlsch_rounds[3];
-  if (dret3x > 0) {
-     if there is a third retransmission, decrease MCS for stabilization and
-     restart averaging window to stabilize transmission
-    bler_stats->last_frame_slot = now;
-    bler_stats->mcs = max(9, bler_stats->mcs - 1);
-    memcpy(bler_stats->dlsch_rounds, stats->dlsch_rounds, sizeof(stats->dlsch_rounds));
-    LOG_D(MAC, "%4d.%2d: %d retx in 3rd round, setting MCS to %d and restarting window\n", frame, slot, dret3x, bler_stats->mcs);
-    return bler_stats->mcs;
-  }*/
-  if (diff < BLER_UPDATE_FRAME * n)
-    return old_mcs; // no update
-
-  // last update is longer than x frames ago
-  const int dtx = (int)(stats->dlsch_rounds[0] - bler_stats->dlsch_rounds[0]);
-  const int dretx = (int)(stats->dlsch_rounds[1] - bler_stats->dlsch_rounds[1]);
-  const int dretx2 = (int)(stats->dlsch_rounds[2] - bler_stats->dlsch_rounds[2]);
-  const float bler_window = dtx > 0 ? (float) dretx / dtx : bler_stats->bler;
-  const float rd2_bler_wnd = dtx > 0 ? (float) dretx2 / dtx : bler_stats->rd2_bler;
-  bler_stats->bler = BLER_FILTER * bler_stats->bler + (1 - BLER_FILTER) * bler_window;
-  bler_stats->rd2_bler = BLER_FILTER / 4 * bler_stats->rd2_bler + (1 - BLER_FILTER / 4) * rd2_bler_wnd;
-  int new_mcs = old_mcs;
-
-  // TODO put back this condition when relevant
-  /* first ensure that number of 2nd retx is below threshold. If this is the
-   * case, use 1st retx to adjust faster
-  if (bler_stats->rd2_bler > nrmac->dl_rd2_bler_threshold && old_mcs > 6) {
-    new_mcs -= 2;
-  } else if (bler_stats->rd2_bler < nrmac->dl_rd2_bler_threshold) {*/
-  if (bler_stats->bler < nrmac->dl_bler_target_lower && old_mcs < max_mcs && dtx > 9)
-    new_mcs += 1;
-  else if (bler_stats->bler > nrmac->dl_bler_target_upper && old_mcs > 6)
-    new_mcs -= 1;
-
-  // else we are within threshold boundaries
-  bler_stats->last_frame_slot = now;
-  bler_stats->mcs = new_mcs;
-  memcpy(bler_stats->dlsch_rounds, stats->dlsch_rounds, sizeof(stats->dlsch_rounds));
-  LOG_D(MAC, "%4d.%2d MCS %d -> %d (dtx %d, dretx %d, BLER wnd %.3f avg %.6f, dretx2 %d, RD2 BLER wnd %.3f avg %.6f)\n",
-        frame, slot, old_mcs, new_mcs, dtx, dretx, bler_window, bler_stats->bler, dretx2, rd2_bler_wnd, bler_stats->rd2_bler);
-  return new_mcs;
-}
-
 void nr_store_dlsch_buffer(module_id_t module_id,
                            frame_t frame,
                            sub_frame_t slot) {
@@ -742,6 +667,7 @@ void pf_dl(module_id_t module_id,
 
     if (sched_ctrl->ul_failure==1 && get_softmodem_params()->phy_test==0) continue;
 
+    const NR_mac_dir_stats_t *stats = &UE_info->mac_stats[UE_id].dl;
     NR_sched_pdsch_t *sched_pdsch = &sched_ctrl->sched_pdsch;
     NR_pdsch_semi_static_t *ps = &sched_ctrl->pdsch_semi_static;
     /* get the PID of a HARQ process awaiting retrnasmission, or -1 otherwise */
@@ -749,7 +675,7 @@ void pf_dl(module_id_t module_id,
     layers[UE_id] = ps->nrOfLayers; // initialization of layers to the previous value in the strcuture
     /* Calculate Throughput */
     const float a = 0.0005f; // corresponds to 200ms window
-    const uint32_t b = UE_info->mac_stats[UE_id].dlsch_current_bytes;
+    const uint32_t b = stats->current_bytes;
     thr_ue[UE_id] = (1 - a) * thr_ue[UE_id] + a * b;
 
     /* retransmission */
@@ -772,8 +698,10 @@ void pf_dl(module_id_t module_id,
         continue;
 
       /* Calculate coeff */
-      set_dl_mcs(sched_pdsch,sched_ctrl,&mac->dl_max_mcs,ps->mcsTableIdx);
-      sched_pdsch->mcs = get_mcs_from_bler(module_id, /* CC_id = */ 0, frame, slot, UE_id, ps->mcsTableIdx);
+      const NR_bler_options_t *bo = &mac->dl_bler;
+      const int max_mcs_table = ps->mcsTableIdx == 1 ? 27 : 28;
+      const int max_mcs = min(sched_ctrl->dl_max_mcs, max_mcs_table);
+      sched_pdsch->mcs = get_mcs_from_bler(bo, stats, &sched_ctrl->dl_bler_stats, max_mcs, frame);
       layers[UE_id] = set_dl_nrOfLayers(sched_ctrl);
       const uint8_t Qm = nr_get_Qm_dl(sched_pdsch->mcs, ps->mcsTableIdx);
       const uint16_t R = nr_get_code_rate_dl(sched_pdsch->mcs, ps->mcsTableIdx);
@@ -1091,7 +1019,7 @@ void nr_schedule_ue_spec(module_id_t module_id,
     if (sched_ctrl->ul_failure==1 && get_softmodem_params()->phy_test==0) continue;
 
     NR_sched_pdsch_t *sched_pdsch = &sched_ctrl->sched_pdsch;
-    UE_info->mac_stats[UE_id].dlsch_current_bytes = 0;
+    UE_info->mac_stats[UE_id].dl.current_bytes = 0;
     NR_CellGroupConfig_t *cg = UE_info->CellGroup[UE_id];
 
     NR_BWP_DownlinkDedicated_t *bwpd =
@@ -1149,7 +1077,7 @@ void nr_schedule_ue_spec(module_id_t module_id,
     harq->feedback_frame = pucch->frame;
     harq->feedback_slot = pucch->ul_slot;
     harq->is_waiting = true;
-    UE_info->mac_stats[UE_id].dlsch_rounds[harq->round]++;
+    UE_info->mac_stats[UE_id].dl.rounds[harq->round]++;
     LOG_D(NR_MAC,
           "%4d.%2d [DLSCH/PDSCH/PUCCH] UE %d 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,
@@ -1483,7 +1411,7 @@ void nr_schedule_ue_spec(module_id_t module_id,
             lcid_bytes += len;
           }
 
-          UE_info->mac_stats[UE_id].lc_bytes_tx[lcid] += lcid_bytes;
+          UE_info->mac_stats[UE_id].dl.lc_bytes[lcid] += lcid_bytes;
         }
       } else if (get_softmodem_params()->phy_test || get_softmodem_params()->do_ra) {
         /* we will need the large header, phy-test typically allocates all
@@ -1521,8 +1449,10 @@ void nr_schedule_ue_spec(module_id_t module_id,
         buf=bufEnd;
       }
 
-      UE_info->mac_stats[UE_id].dlsch_total_bytes += TBS;
-      UE_info->mac_stats[UE_id].dlsch_current_bytes = TBS;
+      NR_mac_stats_t *mac_stats = &UE_info->mac_stats[UE_id];
+      mac_stats->dl.total_bytes += TBS;
+      mac_stats->dl.current_bytes = TBS;
+
       /* save retransmission information */
       harq->sched_pdsch = *sched_pdsch;
       /* save which time allocation has been used, to be used on
diff --git a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_primitives.c b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_primitives.c
index 86349146a0dfe74db80a3b5764434be16409eeb3..9141bc78b43979337a8a2a72e4b7417071b486e9 100644
--- a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_primitives.c
+++ b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_primitives.c
@@ -145,7 +145,6 @@ uint8_t set_dl_nrOfLayers(NR_UE_sched_ctrl_t *sched_ctrl) {
 
 }
 
-
 uint16_t set_pm_index(NR_UE_sched_ctrl_t *sched_ctrl,
                       int layers,
                       int N1, int N2,
@@ -169,51 +168,45 @@ uint16_t set_pm_index(NR_UE_sched_ctrl_t *sched_ctrl,
     AssertFatal(1==0,"More than 2 antenna ports not yet supported\n");
 }
 
+uint8_t get_mcs_from_cqi(int mcs_table, int cqi_table, int cqi_idx)
+{
+  if (cqi_idx <= 0) {
+    LOG_E(NR_MAC, "invalid cqi_idx %d, default to MCS 9\n", cqi_idx);
+    return 9;
+  }
 
-void set_dl_mcs(NR_sched_pdsch_t *sched_pdsch,
-                NR_UE_sched_ctrl_t *sched_ctrl,
-                uint8_t *target_mcs,
-                uint8_t mcs_table_idx) {
-
-  if (sched_ctrl->set_mcs) {
-    // TODO for wideband case and multiple TB
-    int cqi_idx = sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.wb_cqi_1tb;
-    uint16_t target_coderate,target_qm;
-    if (cqi_idx>0) {
-      int cqi_table = sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.cqi_table;
-      if (cqi_table != mcs_table_idx)
-       LOG_W(NR_MAC,"Indices of MCS tables don't correspond yet, cri_ri_li_pmi_cqi_report.cqi_table %d, mcs_table_index %d\n",cqi_table,mcs_table_idx);
-      switch (cqi_table) {
-        case 0:
-          target_qm = cqi_table1[cqi_idx][0];
-          target_coderate = cqi_table1[cqi_idx][1];
-          break;
-        case 1:
-          target_qm = cqi_table2[cqi_idx][0];
-          target_coderate = cqi_table2[cqi_idx][1];
-          break;
-        case 2:
-          target_qm = cqi_table3[cqi_idx][0];
-          target_coderate = cqi_table3[cqi_idx][1];
-          break;
-        default:
-          AssertFatal(1==0,"Invalid cqi table index %d\n",cqi_table);
-      }
-      int max_mcs = 28;
-      int R,Qm;
-      if (mcs_table_idx == 1)
-        max_mcs = 27;
-      for (int i=0; i<=max_mcs; i++) {
-        R = nr_get_code_rate_dl(i, mcs_table_idx);
-        Qm = nr_get_Qm_dl(i, mcs_table_idx);
-        if ((Qm == target_qm) && (target_coderate <= R)) {
-          *target_mcs = i;
-          break;
-        }
-      }
-    }
-    sched_ctrl->set_mcs = false;
+  if (mcs_table != cqi_table) {
+    LOG_E(NR_MAC, "indices of CQI (%d) and MCS (%d) tables don't correspond yet\n", cqi_table, mcs_table);
+    return 9;
   }
+
+  uint16_t target_coderate, target_qm;
+  switch (cqi_table) {
+    case 0:
+      target_qm = cqi_table1[cqi_idx][0];
+      target_coderate = cqi_table1[cqi_idx][1];
+      break;
+    case 1:
+      target_qm = cqi_table2[cqi_idx][0];
+      target_coderate = cqi_table2[cqi_idx][1];
+      break;
+    case 2:
+      target_qm = cqi_table3[cqi_idx][0];
+      target_coderate = cqi_table3[cqi_idx][1];
+      break;
+    default:
+      AssertFatal(1==0,"Invalid cqi table index %d\n",cqi_table);
+  }
+  const int max_mcs = mcs_table == 1 ? 27 : 28;
+  for (int i = 0; i <= max_mcs; i++) {
+    const int R = nr_get_code_rate_dl(i, mcs_table);
+    const int Qm = nr_get_Qm_dl(i, mcs_table);
+    if (Qm == target_qm && target_coderate <= R)
+      return i;
+  }
+
+  LOG_E(NR_MAC, "could not find maximum MCS from cqi_idx %d, default to 9\n", cqi_idx);
+  return 9;
 }
 
 void set_dl_dmrs_ports(NR_pdsch_semi_static_t *ps) {
@@ -768,6 +761,51 @@ void nr_set_pusch_semi_static(const NR_SIB1_t *sib1,
       : num_dmrs_cdm_grps_no_data * 4;
 }
 
+#define BLER_UPDATE_FRAME 10
+#define BLER_FILTER 0.9f
+int get_mcs_from_bler(const NR_bler_options_t *bler_options,
+                      const NR_mac_dir_stats_t *stats,
+                      NR_bler_stats_t *bler_stats,
+                      int max_mcs,
+                      frame_t frame)
+{
+  /* first call: everything is zero. Initialize to sensible default */
+  if (bler_stats->last_frame == 0 && bler_stats->mcs == 0) {
+    bler_stats->last_frame = frame;
+    bler_stats->mcs = 9;
+    bler_stats->bler = (bler_options->lower + bler_options->upper) / 2.0f;
+  }
+  int diff = frame - bler_stats->last_frame;
+  if (diff < 0) // wrap around
+    diff += 1024;
+
+  max_mcs = min(max_mcs, 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
+
+  // last update is longer than x frames ago
+  const int dtx = (int)(stats->rounds[0] - bler_stats->rounds[0]);
+  const int dretx = (int)(stats->rounds[1] - bler_stats->rounds[1]);
+  const float bler_window = dtx > 0 ? (float) dretx / dtx : bler_stats->bler;
+  bler_stats->bler = BLER_FILTER * bler_stats->bler + (1 - BLER_FILTER) * bler_window;
+
+  int new_mcs = old_mcs;
+  if (bler_stats->bler < bler_options->lower && old_mcs < max_mcs && dtx > 9)
+    new_mcs += 1;
+  else if ((bler_stats->bler > bler_options->upper && old_mcs > 6) // above threshold
+      || (dtx <= 3 && old_mcs > 9))                                // no activity
+    new_mcs -= 1;
+  // else we are within threshold boundaries
+
+  bler_stats->last_frame = frame;
+  bler_stats->mcs = new_mcs;
+  memcpy(bler_stats->rounds, stats->rounds, sizeof(stats->rounds));
+  LOG_D(MAC, "frame %4d MCS %d -> %d (dtx %d, dretx %d, BLER wnd %.3f avg %.6f)\n",
+        frame, old_mcs, new_mcs, dtx, dretx, bler_window, bler_stats->bler);
+  return new_mcs;
+}
+
 void nr_configure_css_dci_initial(nfapi_nr_dl_tti_pdcch_pdu_rel15_t* pdcch_pdu,
 				  nr_scs_e scs_common,
 				  nr_scs_e pdcch_scs,
@@ -2359,7 +2397,7 @@ int add_new_nr_ue(module_id_t mod_idP, rnti_t rntiP, NR_CellGroupConfig_t *CellG
       compute_csi_bitlen (CellGroup->spCellConfig->spCellConfigDedicated->csi_MeasConfig->choice.setup, UE_info, UE_id, mod_idP);
     NR_UE_sched_ctrl_t *sched_ctrl = &UE_info->UE_sched_ctrl[UE_id];
     memset(sched_ctrl, 0, sizeof(*sched_ctrl));
-    sched_ctrl->set_mcs = true;
+    sched_ctrl->dl_max_mcs = 28; /* do not limit MCS for individual UEs */
     sched_ctrl->set_pmi = false;
     sched_ctrl->ta_frame = 0;
     sched_ctrl->ta_update = 31;
diff --git a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_uci.c b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_uci.c
index a29aa8018cd5b679da1a3f7d3f82fe5cfd8f9cc2..b9292876e350101907409e9843e5c1f3166cd64b 100644
--- a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_uci.c
+++ b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_uci.c
@@ -838,8 +838,8 @@ static void handle_dl_harq(module_id_t mod_id,
     harq->round = 0;
     harq->ndi ^= 1;
     NR_mac_stats_t *stats = &UE_info->mac_stats[UE_id];
-    stats->dlsch_errors++;
-    LOG_D(NR_MAC, "retransmission error for UE %d (total %"PRIu64")\n", UE_id, stats->dlsch_errors);
+    stats->dl.errors++;
+    LOG_D(NR_MAC, "retransmission error for UE %d (total %"PRIu64")\n", UE_id, stats->dl.errors);
   } else {
     LOG_D(PHY,"NACK for: pid %d, ue %x\n",harq_pid, UE_id);
     add_tail_nr_list(&UE_info->UE_sched_ctrl[UE_id].retrans_dl_harq, harq_pid);
@@ -1250,7 +1250,12 @@ void evaluate_cqi_report(uint8_t *payload,
     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);
   }
-  sched_ctrl->set_mcs = true;
+
+  // TODO for wideband case and multiple TB
+  const int cqi_idx = sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.wb_cqi_1tb;
+  const int mcs_table = sched_ctrl->pdsch_semi_static.mcsTableIdx;
+  const int cqi_table = sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.cqi_table;
+  sched_ctrl->dl_max_mcs = get_mcs_from_cqi(mcs_table, cqi_table, cqi_idx);
 }
 
 
diff --git a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_ulsch.c b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_ulsch.c
index 2fce141e7e2c5a37739f18943ec48bd1483eb97a..747808bfa893e100f43158e4e3371d6a8fda2a8a 100644
--- a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_ulsch.c
+++ b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_ulsch.c
@@ -464,7 +464,7 @@ int nr_process_mac_pdu(module_id_t module_idP,
                 rx_lcid,
                 module_idP,
                 mac_len);
-          UE_info->mac_stats[UE_id].lc_bytes_rx[rx_lcid] += mac_len;
+          UE_info->mac_stats[UE_id].ul.lc_bytes[rx_lcid] += mac_len;
 
           mac_rlc_data_ind(module_idP,
                            UE_info->rnti[UE_id],
@@ -526,7 +526,7 @@ void abort_nr_ul_harq(module_id_t mod_id, int UE_id, int8_t harq_pid)
 
   harq->ndi ^= 1;
   harq->round = 0;
-  UE_info->mac_stats[UE_id].ulsch_errors++;
+  UE_info->mac_stats[UE_id].ul.errors++;
   add_tail_nr_list(&sched_ctrl->available_ul_harq, harq_pid);
 
   /* the transmission failed: the UE won't send the data we expected initially,
@@ -637,7 +637,7 @@ void nr_rx_sdu(const module_id_t gnb_mod_idP,
         T_INT(rntiP), T_INT(frameP), T_INT(slotP), T_INT(harq_pid),
         T_BUFFER(sduP, sdu_lenP));
 
-    UE_info->mac_stats[UE_id].ulsch_total_bytes_rx += sdu_lenP;
+    UE_info->mac_stats[UE_id].ul.total_bytes += sdu_lenP;
     LOG_D(NR_MAC, "[gNB %d][PUSCH %d] CC_id %d %d.%d Received ULSCH sdu from PHY (rnti %x, UE_id %d) ul_cqi %d TA %d sduP %p, rssi %d\n",
           gnb_mod_idP,
           harq_pid,
@@ -1174,10 +1174,11 @@ void pf_ul(module_id_t module_id,
     const uint16_t bwpSize = NRRIV2BW(genericParameters->locationAndBandwidth, MAX_BWP_SIZE);
     NR_sched_pusch_t *sched_pusch = &sched_ctrl->sched_pusch;
     NR_pusch_semi_static_t *ps = &sched_ctrl->pusch_semi_static;
+    const NR_mac_dir_stats_t *stats = &UE_info->mac_stats[UE_id].ul;
 
     /* Calculate throughput */
     const float a = 0.0005f; // corresponds to 200ms window
-    const uint32_t b = UE_info->mac_stats[UE_id].ulsch_current_bytes;
+    const uint32_t b = stats->current_bytes;
     ul_thr_ue[UE_id] = (1 - a) * ul_thr_ue[UE_id] + a * b;
 
     /* Check if retransmission is necessary */
@@ -1209,6 +1210,10 @@ void pf_ul(module_id_t module_id,
       continue;
     }
 
+    const NR_bler_options_t *bo = &nrmac->ul_bler;
+    const int max_mcs = bo->max_mcs; /* no per-user maximum MCS yet */
+    sched_pusch->mcs = get_mcs_from_bler(bo, stats, &sched_ctrl->ul_bler_stats, max_mcs, frame);
+
     /* Schedule UE on SR or UL inactivity and no data (otherwise, will be scheduled
      * based on data to transmit) */
     if (B == 0 && do_sched) {
@@ -1284,7 +1289,7 @@ void pf_ul(module_id_t module_id,
                          sched_ctrl->aggregation_level);
 
       NR_sched_pusch_t *sched_pusch = &sched_ctrl->sched_pusch;
-      sched_pusch->mcs = nrmac->min_grant_mcs;
+      sched_pusch->mcs = min(nrmac->min_grant_mcs, sched_pusch->mcs);
       update_ul_ue_R_Qm(sched_pusch, ps);
       sched_pusch->rbStart = rbStart;
       sched_pusch->rbSize = min_rb;
@@ -1310,7 +1315,6 @@ void pf_ul(module_id_t module_id,
     add_tail_nr_list(&UE_sched, UE_id);
 
     /* Calculate coefficient*/
-    sched_pusch->mcs = nrmac->min_grant_mcs;
     const uint32_t tbs = ul_pf_tbs[ps->mcs_table][sched_pusch->mcs];
     coeff_ue[UE_id] = (float) tbs / ul_thr_ue[UE_id];
     LOG_D(NR_MAC,"b %d, ul_thr_ue[%d] %f, tbs %d, coeff_ue[%d] %f\n",
@@ -1652,7 +1656,8 @@ void nr_schedule_ulsch(module_id_t module_id, frame_t frame, sub_frame_t slot)
                                       cg->spCellConfig->spCellConfigDedicated->uplinkConfig ?
                                       cg->spCellConfig->spCellConfigDedicated->uplinkConfig->initialUplinkBWP : NULL;
 
-    UE_info->mac_stats[UE_id].ulsch_current_bytes = 0;
+    NR_mac_stats_t *mac_stats = &UE_info->mac_stats[UE_id];
+    mac_stats->ul.current_bytes = 0;
 
     /* dynamic PUSCH values (RB alloc, MCS, hence R, Qm, TBS) that change in
      * every TTI are pre-populated by the preprocessor and used below */
@@ -1696,10 +1701,10 @@ void nr_schedule_ulsch(module_id_t module_id, frame_t frame, sub_frame_t slot)
     NR_pusch_semi_static_t *ps = &sched_ctrl->pusch_semi_static;
 
     /* Statistics */
-    AssertFatal(cur_harq->round < 8, "Indexing ulsch_rounds[%d] is out of bounds\n", cur_harq->round);
-    UE_info->mac_stats[UE_id].ulsch_rounds[cur_harq->round]++;
+    AssertFatal(cur_harq->round < 8, "Indexing UL rounds[%d] is out of bounds\n", cur_harq->round);
+    mac_stats->ul.rounds[cur_harq->round]++;
     if (cur_harq->round == 0) {
-      UE_info->mac_stats[UE_id].ulsch_total_bytes_scheduled += sched_pusch->tb_size;
+      mac_stats->ulsch_total_bytes_scheduled += sched_pusch->tb_size;
       /* Save information on MCS, TBS etc for the current initial transmission
        * so we have access to it when retransmitting */
       cur_harq->sched_pusch = *sched_pusch;
@@ -1719,7 +1724,7 @@ void nr_schedule_ulsch(module_id_t module_id, frame_t frame, sub_frame_t slot)
             cur_harq->round,
             cur_harq->ndi);
     }
-    UE_info->mac_stats[UE_id].ulsch_current_bytes = sched_pusch->tb_size;
+    mac_stats->ul.current_bytes = sched_pusch->tb_size;
     sched_ctrl->last_ul_frame = sched_pusch->frame;
     sched_ctrl->last_ul_slot = sched_pusch->slot;
 
diff --git a/openair2/LAYER2/NR_MAC_gNB/mac_proto.h b/openair2/LAYER2/NR_MAC_gNB/mac_proto.h
index cfb6573f4923adb71027b567175d4521846ca6ff..da234fe30a2ef76b397c79eb2ccf8dfe93ec6698 100644
--- a/openair2/LAYER2/NR_MAC_gNB/mac_proto.h
+++ b/openair2/LAYER2/NR_MAC_gNB/mac_proto.h
@@ -507,10 +507,7 @@ uint16_t set_pm_index(NR_UE_sched_ctrl_t *sched_ctrl,
                       int xp_pdsch_antenna_ports,
                       int codebook_mode);
 
-void set_dl_mcs(NR_sched_pdsch_t *sched_pdsch,
-                NR_UE_sched_ctrl_t *sched_ctrl,
-                uint8_t *target_mcs,
-                uint8_t mcs_table_idx);
+uint8_t get_mcs_from_cqi(int mcs_table, int cqi_table, int cqi_idx);
 
 uint8_t set_dl_nrOfLayers(NR_UE_sched_ctrl_t *sched_ctrl);
 
@@ -531,6 +528,12 @@ bool nr_find_nb_rb(uint16_t Qm,
                    uint32_t *tbs,
                    uint16_t *nb_rb);
 
+int get_mcs_from_bler(const NR_bler_options_t *bler_options,
+                      const NR_mac_dir_stats_t *stats,
+                      NR_bler_stats_t *bler_stats,
+                      int max_mcs,
+                      frame_t frame);
+
 void nr_sr_reporting(int Mod_idP, frame_t frameP, sub_frame_t slotP);
 
 void dump_mac_stats(gNB_MAC_INST *gNB, char *output, int strlen, bool reset_rsrp);
diff --git a/openair2/LAYER2/NR_MAC_gNB/main.c b/openair2/LAYER2/NR_MAC_gNB/main.c
index e047494b5296666e55510465702e5f7a72429f68..4c14233a32ad9a658b1850db8713f4e063af8f51 100644
--- a/openair2/LAYER2/NR_MAC_gNB/main.c
+++ b/openair2/LAYER2/NR_MAC_gNB/main.c
@@ -102,9 +102,9 @@ void dump_mac_stats(gNB_MAC_INST *gNB, char *output, int strlen, bool reset_rsrp
 
     stroff+=sprintf(output+stroff,"UE %d: dlsch_rounds %"PRIu64"/%"PRIu64"/%"PRIu64"/%"PRIu64", dlsch_errors %"PRIu64", pucch0_DTX %d, BLER %.5f MCS %d\n",
                     UE_id,
-                    stats->dlsch_rounds[0], stats->dlsch_rounds[1],
-                    stats->dlsch_rounds[2], stats->dlsch_rounds[3],
-                    stats->dlsch_errors,
+                    stats->dl.rounds[0], stats->dl.rounds[1],
+                    stats->dl.rounds[2], stats->dl.rounds[3],
+                    stats->dl.errors,
                     stats->pucch0_DTX,
                     sched_ctrl->dl_bler_stats.bler,
                     sched_ctrl->dl_bler_stats.mcs);
@@ -112,25 +112,27 @@ void dump_mac_stats(gNB_MAC_INST *gNB, char *output, int strlen, bool reset_rsrp
       stats->num_rsrp_meas = 0;
       stats->cumul_rsrp = 0;
     }
-    stroff+=sprintf(output+stroff,"UE %d: dlsch_total_bytes %"PRIu64"\n", UE_id, stats->dlsch_total_bytes);
-    stroff+=sprintf(output+stroff,"UE %d: ulsch_rounds %"PRIu64"/%"PRIu64"/%"PRIu64"/%"PRIu64", ulsch_DTX %d, ulsch_errors %"PRIu64"\n",
+    stroff+=sprintf(output+stroff,"UE %d: dlsch_total_bytes %"PRIu64"\n", UE_id, stats->dl.total_bytes);
+    stroff+=sprintf(output+stroff,"UE %d: ulsch_rounds %"PRIu64"/%"PRIu64"/%"PRIu64"/%"PRIu64", ulsch_DTX %d, ulsch_errors %"PRIu64", BLER %.5f MCS %d\n",
                     UE_id,
-                    stats->ulsch_rounds[0], stats->ulsch_rounds[1],
-                    stats->ulsch_rounds[2], stats->ulsch_rounds[3],
+                    stats->ul.rounds[0], stats->ul.rounds[1],
+                    stats->ul.rounds[2], stats->ul.rounds[3],
                     stats->ulsch_DTX,
-                    stats->ulsch_errors);
+                    stats->ul.errors,
+                    sched_ctrl->ul_bler_stats.bler,
+                    sched_ctrl->ul_bler_stats.mcs);
     stroff+=sprintf(output+stroff,
                     "UE %d: ulsch_total_bytes_scheduled %"PRIu64", ulsch_total_bytes_received %"PRIu64"\n",
                     UE_id,
-                    stats->ulsch_total_bytes_scheduled, stats->ulsch_total_bytes_rx);
+                    stats->ulsch_total_bytes_scheduled, stats->ul.total_bytes);
     for (int lc_id = 0; lc_id < 63; lc_id++) {
-      if (stats->lc_bytes_tx[lc_id] > 0) {
-        stroff+=sprintf(output+stroff, "UE %d: LCID %d: %"PRIu64" bytes TX\n", UE_id, lc_id, stats->lc_bytes_tx[lc_id]);
-	LOG_D(NR_MAC, "UE %d: LCID %d: %"PRIu64" bytes TX\n", UE_id, lc_id, stats->lc_bytes_tx[lc_id]);
+      if (stats->dl.lc_bytes[lc_id] > 0) {
+        stroff+=sprintf(output+stroff, "UE %d: LCID %d: %"PRIu64" bytes TX\n", UE_id, lc_id, stats->dl.lc_bytes[lc_id]);
+	LOG_D(NR_MAC, "UE %d: LCID %d: %"PRIu64" bytes TX\n", UE_id, lc_id, stats->dl.lc_bytes[lc_id]);
       }
-      if (stats->lc_bytes_rx[lc_id] > 0) {
-        stroff+=sprintf(output+stroff, "UE %d: LCID %d: %"PRIu64" bytes RX\n", UE_id, lc_id, stats->lc_bytes_rx[lc_id]);
-	LOG_D(NR_MAC, "UE %d: LCID %d: %"PRIu64" bytes RX\n", UE_id, lc_id, stats->lc_bytes_rx[lc_id]);
+      if (stats->ul.lc_bytes[lc_id] > 0) {
+        stroff+=sprintf(output+stroff, "UE %d: LCID %d: %"PRIu64" bytes RX\n", UE_id, lc_id, stats->ul.lc_bytes[lc_id]);
+	LOG_D(NR_MAC, "UE %d: LCID %d: %"PRIu64" bytes RX\n", UE_id, lc_id, stats->ul.lc_bytes[lc_id]);
       }
     }
   }
diff --git a/openair2/LAYER2/NR_MAC_gNB/nr_mac_gNB.h b/openair2/LAYER2/NR_MAC_gNB/nr_mac_gNB.h
index 9d8e28da9719165f4af5bd7a16dd71a2f86991b0..99095e1dede3e0a64f6132b56e4c59f15862dd5a 100644
--- a/openair2/LAYER2/NR_MAC_gNB/nr_mac_gNB.h
+++ b/openair2/LAYER2/NR_MAC_gNB/nr_mac_gNB.h
@@ -459,13 +459,12 @@ typedef struct NR_UE_harq {
 
 //! fixme : need to enhace for the multiple TB CQI report
 
-typedef struct NR_DL_bler_stats {
-  frame_t last_frame_slot;
+typedef struct NR_bler_stats {
+  frame_t last_frame;
   float bler;
-  float rd2_bler;
   uint8_t mcs;
-  uint64_t dlsch_rounds[8];
-} NR_DL_bler_stats_t;
+  uint64_t rounds[8];
+} NR_bler_stats_t;
 
 //
 /*! As per spec 38.214 section 5.2.1.4.2
@@ -613,6 +612,9 @@ typedef struct {
   NR_pdsch_semi_static_t pdsch_semi_static;
   /// Sched PDSCH: scheduling decisions, copied into HARQ and cleared every TTI
   NR_sched_pdsch_t sched_pdsch;
+  /// UE-estimated maximum MCS (from CSI-RS)
+  uint8_t dl_max_mcs;
+
   /// For UL synchronization: store last UL scheduling grant
   frame_t last_ul_frame;
   sub_frame_t last_ul_slot;
@@ -624,7 +626,8 @@ typedef struct {
   mac_rlc_status_resp_t rlc_status[NR_MAX_NUM_LCID];
 
   /// Estimation of HARQ from BLER
-  NR_DL_bler_stats_t dl_bler_stats;
+  NR_bler_stats_t dl_bler_stats;
+  NR_bler_stats_t ul_bler_stats;
 
   uint16_t ta_frame;
   int16_t ta_update;
@@ -641,7 +644,6 @@ typedef struct {
   int ul_failure;
   struct CSI_Report CSI_report;
   bool SR;
-  bool set_mcs;
   bool set_pmi;
   /// information about every HARQ process
   NR_UE_harq_t harq_processes[NR_MAX_NB_HARQ_PROCESSES];
@@ -674,24 +676,29 @@ typedef struct {
   uicc_t *uicc;
 } NRUEcontext_t;
 
-typedef struct {
-  uint64_t lc_bytes_tx[64];
-  uint64_t lc_bytes_rx[64];
-  uint64_t dlsch_rounds[8];
-  uint64_t dlsch_errors;
-  uint64_t dlsch_total_bytes;
-  int dlsch_current_bytes;
-  uint64_t ulsch_rounds[8];
-  uint64_t ulsch_errors;
+typedef struct NR_mac_dir_stats {
+  uint64_t lc_bytes[64];
+  uint64_t rounds[8];
+  uint64_t errors;
+  uint64_t total_bytes;
+  uint32_t current_bytes;
+} NR_mac_dir_stats_t;
+
+typedef struct NR_mac_stats {
+  NR_mac_dir_stats_t dl;
+  NR_mac_dir_stats_t ul;
   uint32_t ulsch_DTX;
   uint64_t ulsch_total_bytes_scheduled;
-  uint64_t ulsch_total_bytes_rx;
-  int ulsch_current_bytes;
   uint32_t pucch0_DTX;
   int cumul_rsrp;
   uint8_t num_rsrp_meas;
 } NR_mac_stats_t;
 
+typedef struct NR_bler_options {
+  double upper;
+  double lower;
+  uint8_t max_mcs;
+} NR_bler_options_t;
 
 /*! \brief UE list used by gNB to order UEs/CC for scheduling*/
 #define MAX_CSI_REPORTCONFIG 48
@@ -833,10 +840,8 @@ typedef struct gNB_MAC_INST_s {
   int xp_pdsch_antenna_ports;
 
   bool first_MIB;
-  double dl_bler_target_upper;
-  double dl_bler_target_lower;
-  double dl_rd2_bler_threshold;
-  uint8_t dl_max_mcs;
+  NR_bler_options_t dl_bler;
+  NR_bler_options_t ul_bler;
   uint8_t harq_round_max;
   uint8_t min_grant_prb;
   uint8_t min_grant_mcs;