/* MS Power Control Loop L1 */ /* (C) 2014 by Holger Hans Peter Freyther * (C) 2020-2021 by sysmocom - s.m.f.c. GmbH * Author: Vadim Yanitskiy * * All Rights Reserved * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ #include #include #include #include #include #include #include #include #include #include #include /* We don't want to deal with floating point, so we scale up */ #define EWMA_SCALE_FACTOR 100 /* Base Low-Pass Single-Pole IIR Filter (EWMA) formula: * * Avg[n] = a * Val[n] + (1 - a) * Avg[n - 1] * * where parameter 'a' determines how much weight of the latest measurement value * 'Val[n]' carries vs the weight of the accumulated average 'Avg[n - 1]'. The * value of 'a' is usually a float in range 0 .. 1, so: * * - value 0.5 gives equal weight to both 'Val[n]' and 'Avg[n - 1]'; * - value 1.0 means no filtering at all (pass through); * - value 0.0 makes no sense. * * Further optimization: * * Avg[n] = a * Val[n] + Avg[n - 1] - a * Avg[n - 1] * ^^^^^^ ^^^^^^^^^^ * * a) this can be implemented in C using '+=' operator: * * Avg += a * Val - a * Avg * Avg += a * (Val - Avg) * * b) everything is scaled up by 100 to avoid floating point stuff: * * Avg100 += A * (Val - Avg) * * where 'Avg100' is 'Avg * 100' and 'A' is 'a * 100'. * * For more details, see: * * https://en.wikipedia.org/wiki/Moving_average * https://en.wikipedia.org/wiki/Low-pass_filter#Simple_infinite_impulse_response_filter * https://tomroelandts.com/articles/low-pass-single-pole-iir-filter */ static int do_pf_ewma(const struct gsm_power_ctrl_meas_params *mp, struct gsm_power_ctrl_meas_proc_state *mps, const int Val) { const uint8_t A = mp->ewma.alpha; int *Avg100 = &mps->ewma.Avg100; /* We don't have 'Avg[n - 1]' if this is the first run */ if (mps->meas_num++ == 0) { *Avg100 = Val * EWMA_SCALE_FACTOR; return Val; } *Avg100 += A * (Val - *Avg100 / EWMA_SCALE_FACTOR); return *Avg100 / EWMA_SCALE_FACTOR; } /* Calculate target RxLev value from lower/upper thresholds */ #define CALC_TARGET(mp) \ (mp.lower_thresh + mp.upper_thresh) / 2 /* Calculate a 'delta' value (for the given MS/BS power control state and parameters) * to be applied to the current Tx power level to approach the target level. */ static int calc_delta(const struct gsm_power_ctrl_params *params, struct lchan_power_ctrl_state *state, const int rxlev_dbm) { int rxlev_dbm_avg; uint8_t rxlev_avg; int delta; /* Filter RxLev value to reduce unnecessary Tx power oscillations */ switch (params->rxlev_meas.algo) { case GSM_PWR_CTRL_MEAS_AVG_ALGO_OSMO_EWMA: rxlev_dbm_avg = do_pf_ewma(¶ms->rxlev_meas, &state->rxlev_meas_proc, rxlev_dbm); break; /* TODO: implement other pre-processing methods */ case GSM_PWR_CTRL_MEAS_AVG_ALGO_NONE: default: /* No filtering (pass through) */ rxlev_dbm_avg = rxlev_dbm; } /* FIXME: avoid this conversion, accept RxLev as-is */ rxlev_avg = dbm2rxlev(rxlev_dbm_avg); /* Check if RxLev is within the threshold window */ if (rxlev_avg >= params->rxlev_meas.lower_thresh && rxlev_avg <= params->rxlev_meas.upper_thresh) return 0; /* How many dBs measured power should be increased (+) or decreased (-) * to reach expected power. */ delta = CALC_TARGET(params->rxlev_meas) - rxlev_avg; /* Don't ever change more than PWR_{LOWER,RAISE}_MAX_DBM during one loop * iteration, i.e. reduce the speed at which the MS transmit power can * change. A higher value means a lower level (and vice versa) */ if (delta > params->inc_step_size_db) delta = params->inc_step_size_db; else if (delta < -params->red_step_size_db) delta = -params->red_step_size_db; return delta; } /*! compute the new MS POWER LEVEL communicated to the MS and store it in lchan. * \param lchan logical channel for which to compute (and in which to store) new power value. * \param[in] ms_power_lvl MS Power Level received from Uplink L1 SACCH Header in SACCH block. * \param[in] ul_rssi_dbm Signal level of the received SACCH block, in dBm. */ int lchan_ms_pwr_ctrl(struct gsm_lchan *lchan, const uint8_t ms_power_lvl, const int8_t ul_rssi_dbm) { struct lchan_power_ctrl_state *state = &lchan->ms_power_ctrl; const struct gsm_power_ctrl_params *params = state->dpc_params; struct gsm_bts_trx *trx = lchan->ts->trx; struct gsm_bts *bts = trx->bts; enum gsm_band band = bts->band; int8_t new_power_lvl; /* TS 05.05 power level */ int8_t ms_dbm, new_dbm, current_dbm, bsc_max_dbm; if (!trx_ms_pwr_ctrl_is_osmo(trx)) return 0; if (params == NULL) return 0; /* Power control interval: how many blocks do we skip? */ if (state->skip_block_num-- > 0) return 0; /* Reset the number of SACCH blocks to be skipped: * ctrl_interval=0 => 0 blocks to skip, * ctrl_interval=1 => 1 blocks to skip, * ctrl_interval=2 => 3 blocks to skip, * so basically ctrl_interval * 2 - 1. */ state->skip_block_num = params->ctrl_interval * 2 - 1; ms_dbm = ms_pwr_dbm(band, ms_power_lvl); if (ms_dbm < 0) { LOGPLCHAN(lchan, DLOOP, LOGL_NOTICE, "Failed to calculate dBm for power ctl level %" PRIu8 " on band %s\n", ms_power_lvl, gsm_band_name(band)); return 0; } bsc_max_dbm = ms_pwr_dbm(band, state->max); if (bsc_max_dbm < 0) { LOGPLCHAN(lchan, DLOOP, LOGL_NOTICE, "Failed to calculate dBm for power ctl level %" PRIu8 " on band %s\n", state->max, gsm_band_name(band)); return 0; } /* Calculate the new Tx power value (in dBm) */ new_dbm = ms_dbm + calc_delta(params, state, ul_rssi_dbm); /* Make sure new_dbm is never negative. ms_pwr_ctl_lvl() can later on cope with any unsigned dbm value, regardless of band minimal value. */ if (new_dbm < 0) new_dbm = 0; /* Don't ask for smaller ms power level than the one set by BSC upon RSL CHAN ACT */ if (new_dbm > bsc_max_dbm) new_dbm = bsc_max_dbm; new_power_lvl = ms_pwr_ctl_lvl(band, new_dbm); if (new_power_lvl < 0) { LOGPLCHAN(lchan, DLOOP, LOGL_NOTICE, "Failed to retrieve power level for %" PRId8 " dBm on band %d\n", new_dbm, band); return 0; } /* FIXME: this is only needed for logging, print thresholds instead */ int target_dbm = rxlev2dbm(CALC_TARGET(params->rxlev_meas)); if (state->current == new_power_lvl) { LOGPLCHAN(lchan, DLOOP, LOGL_INFO, "Keeping MS power at control level %d, %d dBm " "(rx-ms-pwr-lvl %" PRIu8 ", max-ms-pwr-lvl %" PRIu8 ", rx-current %d dBm, rx-target %d dBm)\n", new_power_lvl, new_dbm, ms_power_lvl, state->max, ul_rssi_dbm, target_dbm); return 0; } current_dbm = ms_pwr_dbm(band, state->current); LOGPLCHAN(lchan, DLOOP, LOGL_INFO, "%s MS power from control level %d (%d dBm) to %d, %d dBm " "(rx-ms-pwr-lvl %" PRIu8 ", max-ms-pwr-lvl %" PRIu8 ", rx-current %d dBm, rx-target %d dBm)\n", (new_dbm > current_dbm) ? "Raising" : "Lowering", state->current, current_dbm, new_power_lvl, new_dbm, ms_power_lvl, state->max, ul_rssi_dbm, target_dbm); /* store the resulting new MS power level in the lchan */ state->current = new_power_lvl; bts_model_adjst_ms_pwr(lchan); return 1; } /*! compute the new Downlink attenuation value for the given logical channel. * \param lchan logical channel for which to compute (and in which to store) new power value. * \param[in] gh pointer to the beginning of (presumably) a Measurement Report. */ int lchan_bs_pwr_ctrl(struct gsm_lchan *lchan, const struct gsm48_hdr *gh) { struct lchan_power_ctrl_state *state = &lchan->bs_power_ctrl; const struct gsm_power_ctrl_params *params = state->dpc_params; uint8_t rxqual_full, rxqual_sub; uint8_t rxlev_full, rxlev_sub; uint8_t rxqual, rxlev; int delta, new; /* Check if dynamic BS Power Control is enabled */ if (params == NULL) return 0; /* Check if this is a Measurement Report */ if (gh->proto_discr != GSM48_PDISC_RR) return 0; if (gh->msg_type != GSM48_MT_RR_MEAS_REP) return 0; /* Check if the measurement results are valid */ if ((gh->data[1] & 0x40) == 0x40) { LOGPLCHAN(lchan, DLOOP, LOGL_DEBUG, "The measurement results are not valid\n"); return 0; } /* See 3GPP TS 44.018, section 10.5.2.20 */ rxqual_full = (gh->data[2] >> 4) & 0x7; rxqual_sub = (gh->data[2] >> 1) & 0x7; rxlev_full = gh->data[0] & 0x3f; rxlev_sub = gh->data[1] & 0x3f; LOGPLCHAN(lchan, DLOOP, LOGL_DEBUG, "Rx DL Measurement Report: " "RXLEV-FULL(%02u), RXQUAL-FULL(%u), " "RXLEV-SUB(%02u), RXQUAL-SUB(%u), " "DTx is %s => using %s\n", rxlev_full, rxqual_full, rxlev_sub, rxqual_sub, lchan->tch.dtx.dl_active ? "enabled" : "disabled", lchan->tch.dtx.dl_active ? "SUB" : "FULL"); /* Power control interval: how many blocks do we skip? */ if (state->skip_block_num-- > 0) return 0; /* Reset the number of SACCH blocks to be skipped: * ctrl_interval=0 => 0 blocks to skip, * ctrl_interval=1 => 1 blocks to skip, * ctrl_interval=2 => 3 blocks to skip, * so basically ctrl_interval * 2 - 1. */ state->skip_block_num = params->ctrl_interval * 2 - 1; /* If DTx is active on Downlink, use the '-SUB' */ if (lchan->tch.dtx.dl_active) { rxqual = rxqual_sub; rxlev = rxlev_sub; } else { /* ... otherwise use the '-FULL' */ rxqual = rxqual_full; rxlev = rxlev_full; } /* If RxQual > L_RXQUAL_XX_P, try to increase Tx power */ if (rxqual > params->rxqual_meas.lower_thresh) { uint8_t old = state->current; /* Tx power has reached the maximum, nothing to do */ if (state->current == 0) return 0; /* Increase Tx power by reducing Tx attenuation */ if (state->current >= params->inc_step_size_db) state->current -= params->inc_step_size_db; else state->current = 0; LOGPLCHAN(lchan, DLOOP, LOGL_INFO, "Reducing Downlink attenuation: " "%u -> %d dB due to RxQual %u worse than L_RXQUAL_XX_P %u\n", old, state->current, rxqual, params->rxqual_meas.lower_thresh); return 1; } /* Calculate a 'delta' for the current attenuation level */ delta = calc_delta(params, state, rxlev2dbm(rxlev)); /* Basic signal transmission / reception formula: * * RxLev = TxPwr - (PathLoss + TxAtt) * * Here we want to change RxLev at the MS side, so: * * RxLev + Delta = TxPwr - (PathLoss + TxAtt) + Delta * * The only parameter we can change here is TxAtt, so: * * RxLev + Delta = TxPwr - PathLoss - TxAtt + Delta * RxLev + Delta = TxPwr - PathLoss - (TxAtt - Delta) */ new = state->current - delta; if (new > state->max) new = state->max; if (new < 0) new = 0; if (state->current != new) { LOGPLCHAN(lchan, DLOOP, LOGL_INFO, "Changing Downlink attenuation: " "%u -> %u dB (maximum %u dB, suggested delta %d dB, " "RxLev current %u (%d dBm), thresholds %u .. %u)\n", state->current, new, state->max, -delta, rxlev, rxlev2dbm(rxlev), params->rxlev_meas.lower_thresh, params->rxlev_meas.upper_thresh); state->current = new; return 1; } else { LOGPLCHAN(lchan, DLOOP, LOGL_INFO, "Keeping Downlink attenuation " "at %u dB (maximum %u dB, suggested delta %d dB, " "RxLev current %u (%d dBm), thresholds %u .. %u)\n", state->current, state->max, -delta, rxlev, rxlev2dbm(rxlev), params->rxlev_meas.lower_thresh, params->rxlev_meas.upper_thresh); return 0; } }