aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--include/osmo-bts/gsm_data.h6
-rw-r--r--include/osmo-bts/power_control.h3
-rw-r--r--src/common/l1sap.c1
-rw-r--r--src/common/power_control.c102
-rw-r--r--src/common/rsl.c52
-rw-r--r--src/common/scheduler.c2
-rw-r--r--src/common/tx_power.c4
-rw-r--r--src/common/vty.c4
-rw-r--r--tests/power/Makefile.am8
-rw-r--r--tests/power/bs_power_loop_test.c398
-rw-r--r--tests/power/bs_power_loop_test.err102
-rw-r--r--tests/power/bs_power_loop_test.ok214
-rw-r--r--tests/testsuite.at7
14 files changed, 878 insertions, 26 deletions
diff --git a/.gitignore b/.gitignore
index 22a1bddd..f76bdac7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -64,6 +64,7 @@ tests/handover/handover_test
tests/tx_power/tx_power_test
tests/ta_control/ta_control_test
tests/power/ms_power_loop_test
+tests/power/bs_power_loop_test
tests/testsuite
tests/testsuite.log
diff --git a/include/osmo-bts/gsm_data.h b/include/osmo-bts/gsm_data.h
index 04c66298..6af96fe9 100644
--- a/include/osmo-bts/gsm_data.h
+++ b/include/osmo-bts/gsm_data.h
@@ -316,11 +316,9 @@ struct gsm_lchan {
/* RTP header Marker bit to indicate beginning of speech after pause */
bool rtp_tx_marker;
- /* MS power control */
+ /* MS/BS power control */
struct lchan_power_ctrl_state ms_power_ctrl;
-
- /* BTS power reduction (in dB) */
- uint8_t bs_power_red;
+ struct lchan_power_ctrl_state bs_power_ctrl;
struct msgb *pending_rel_ind_msg;
diff --git a/include/osmo-bts/power_control.h b/include/osmo-bts/power_control.h
index cb566a8d..f2e14cfe 100644
--- a/include/osmo-bts/power_control.h
+++ b/include/osmo-bts/power_control.h
@@ -6,3 +6,6 @@
int lchan_ms_pwr_ctrl(struct gsm_lchan *lchan,
const uint8_t ms_power_lvl,
const int8_t ul_rssi_dbm);
+
+int lchan_bs_pwr_ctrl(struct gsm_lchan *lchan,
+ const struct gsm48_hdr *gh);
diff --git a/src/common/l1sap.c b/src/common/l1sap.c
index 2038fbad..33d10a5a 100644
--- a/src/common/l1sap.c
+++ b/src/common/l1sap.c
@@ -1546,6 +1546,7 @@ static int l1sap_ph_data_ind(struct gsm_bts_trx *trx,
lchan->meas.flags |= LC_UL_M_F_L1_VALID;
lchan_ms_pwr_ctrl(lchan, data[0] & 0x1f, data_ind->rssi);
+ lchan_bs_pwr_ctrl(lchan, (const struct gsm48_hdr *) &data[5]);
} else
le = &lchan->lapdm_ch.lapdm_dcch;
diff --git a/src/common/power_control.c b/src/common/power_control.c
index 34414392..4c4e2831 100644
--- a/src/common/power_control.c
+++ b/src/common/power_control.c
@@ -209,3 +209,105 @@ int lchan_ms_pwr_ctrl(struct gsm_lchan *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 gsm_bts_trx *trx = lchan->ts->trx;
+ struct gsm_bts *bts = trx->bts;
+ uint8_t rxqual_full, rxqual_sub;
+ uint8_t rxlev_full, rxlev_sub;
+ uint8_t rxqual, rxlev;
+ int delta, new;
+
+ const struct bts_power_ctrl_params *params = &bts->dl_power_ctrl;
+ struct lchan_power_ctrl_state *state = &lchan->bs_power_ctrl;
+
+ /* Check if BS Power Control is enabled */
+ if (state->fixed)
+ 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");
+
+ /* 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;
+ }
+
+ /* Bit Error Rate > 0 => reduce by 2 */
+ if (rxqual > 0) {
+ LOGPLCHAN(lchan, DLOOP, LOGL_INFO, "Reducing Downlink attenuation "
+ "by half: %u -> %u dB due to RXQUAL %u > 0\n",
+ state->current, state->current / 2, rxqual);
+ state->current /= 2;
+ 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, target %d dBm, delta %d dB)\n",
+ state->current, new, state->max, params->target, delta);
+ state->current = new;
+ return 1;
+ } else {
+ LOGPLCHAN(lchan, DLOOP, LOGL_INFO, "Keeping Downlink attenuation "
+ "at %u dB (maximum %u dB, target %d dBm, delta %d dB)\n",
+ state->current, state->max, params->target, delta);
+ return 0;
+ }
+}
diff --git a/src/common/rsl.c b/src/common/rsl.c
index 8760c242..2ebfb323 100644
--- a/src/common/rsl.c
+++ b/src/common/rsl.c
@@ -1036,8 +1036,8 @@ static void clear_lchan_for_pdch_activ(struct gsm_lchan *lchan)
lchan->tch_mode = 0;
memset(&lchan->encr, 0, sizeof(lchan->encr));
memset(&lchan->ho, 0, sizeof(lchan->ho));
- lchan->bs_power_red = 0;
memset(&lchan->ms_power_ctrl, 0, sizeof(lchan->ms_power_ctrl));
+ memset(&lchan->bs_power_ctrl, 0, sizeof(lchan->bs_power_ctrl));
lchan->rqd_ta = 0;
copy_sacch_si_to_lchan(lchan);
memset(&lchan->tch, 0, sizeof(lchan->tch));
@@ -1153,11 +1153,16 @@ static int rsl_rx_chan_activ(struct msgb *msg)
LOGPLCHAN(lchan, DRSL, LOGL_DEBUG, "rx Channel Activation in state: %s.\n",
gsm_lchans_name(lchan->state));
- /* Initialize channel defaults */
+ /* Initialize MS Power Control defaults */
lchan->ms_power_ctrl.max = ms_pwr_ctl_lvl(lchan->ts->trx->bts->band, 0);
lchan->ms_power_ctrl.current = lchan->ms_power_ctrl.max;
lchan->ms_power_ctrl.fixed = true;
+ /* Initialize BS Power Control defaults */
+ lchan->bs_power_ctrl.max = 2 * 15;
+ lchan->bs_power_ctrl.current = 0;
+ lchan->bs_power_ctrl.fixed = true;
+
rsl_tlv_parse(&tp, msgb_l3(msg), msgb_l3len(msg));
/* 9.3.3 Activation Type */
@@ -1209,9 +1214,11 @@ static int rsl_rx_chan_activ(struct msgb *msg)
return rsl_tx_chan_act_nack(lchan, RSL_ERR_SERV_OPT_UNIMPL);
}
- lchan->bs_power_red = BS_POWER2DB(*TLVP_VAL(&tp, RSL_IE_BS_POWER));
+ lchan->bs_power_ctrl.max = BS_POWER2DB(*TLVP_VAL(&tp, RSL_IE_BS_POWER));
+ lchan->bs_power_ctrl.current = lchan->bs_power_ctrl.max;
+
LOGPLCHAN(lchan, DRSL, LOGL_DEBUG, "BS Power attenuation %u dB\n",
- lchan->bs_power_red);
+ lchan->bs_power_ctrl.current);
}
/* 9.3.13 MS Power */
@@ -1224,7 +1231,6 @@ static int rsl_rx_chan_activ(struct msgb *msg)
if (TLVP_PRES_LEN(&tp, RSL_IE_TIMING_ADVANCE, 1))
lchan->rqd_ta = *TLVP_VAL(&tp, RSL_IE_TIMING_ADVANCE);
- /* 9.3.32 BS Power Parameters */
/* 9.3.31 MS Power Parameters */
if (TLVP_PRESENT(&tp, RSL_IE_MS_POWER_PARAM)) {
/* Spec explicitly states BTS should only perform
@@ -1232,6 +1238,14 @@ static int rsl_rx_chan_activ(struct msgb *msg)
* Parameters' IE is present! */
lchan->ms_power_ctrl.fixed = false;
}
+
+ /* 9.3.32 BS Power Parameters */
+ if (TLVP_PRESENT(&tp, RSL_IE_BS_POWER_PARAM)) {
+ /* NOTE: it's safer to start from 0 */
+ lchan->bs_power_ctrl.current = 0;
+ lchan->bs_power_ctrl.fixed = false;
+ }
+
/* 9.3.16 Physical Context */
/* 9.3.29 SACCH Information */
@@ -1753,7 +1767,7 @@ static int rsl_rx_bs_pwr_ctrl(struct msgb *msg)
struct abis_rsl_dchan_hdr *dch = msgb_l2(msg);
struct gsm_lchan *lchan = msg->lchan;
struct tlv_parsed tp;
- uint8_t old_bs_power_red;
+ uint8_t old, new;
rsl_tlv_parse(&tp, msgb_l3(msg), msgb_l3len(msg));
@@ -1766,18 +1780,24 @@ static int rsl_rx_bs_pwr_ctrl(struct msgb *msg)
return rsl_tx_error_report(msg->trx, RSL_ERR_SERV_OPT_UNIMPL, &dch->chan_nr, NULL, msg);
}
- old_bs_power_red = lchan->bs_power_red;
- lchan->bs_power_red = BS_POWER2DB(*TLVP_VAL(&tp, RSL_IE_BS_POWER));
-
- LOGPLCHAN(lchan, DRSL, LOGL_INFO, "BS POWER CONTROL Attenuation %d -> %d dB\n",
- old_bs_power_red, lchan->bs_power_red);
+ new = BS_POWER2DB(*TLVP_VAL(&tp, RSL_IE_BS_POWER));
+ old = lchan->bs_power_ctrl.current;
/* 9.3.31 MS Power Parameters (O) */
if (TLVP_PRESENT(&tp, RSL_IE_BS_POWER_PARAM)) {
- /* Spec explicitly states BTS should perform autonomous
- * BS power control loop in BTS if 'BS Power Parameters'
- * IE is present! WE don't support that. */
- return rsl_tx_error_report(msg->trx, RSL_ERR_OPT_IE_ERROR, &dch->chan_nr, NULL, msg);
+ /* NOTE: it's safer to start from 0 */
+ lchan->bs_power_ctrl.current = 0;
+ lchan->bs_power_ctrl.max = new;
+ lchan->bs_power_ctrl.fixed = false;
+ } else {
+ lchan->bs_power_ctrl.current = new;
+ lchan->bs_power_ctrl.fixed = true;
+ }
+
+ if (lchan->bs_power_ctrl.current != old) {
+ LOGPLCHAN(lchan, DRSL, LOGL_INFO, "BS POWER CONTROL: "
+ "attenuation change %u -> %u dB\n",
+ old, lchan->bs_power_ctrl.current);
}
return 0;
@@ -2975,7 +2995,7 @@ int rsl_tx_meas_res(struct gsm_lchan *lchan, uint8_t *l3, int l3_len, const stru
msgb_tlv_put(msg, RSL_IE_UPLINK_MEAS, ie_len, meas_res);
lchan->meas.flags &= ~LC_UL_M_F_RES_VALID;
}
- msgb_tv_put(msg, RSL_IE_BS_POWER, lchan->bs_power_red / 2);
+ msgb_tv_put(msg, RSL_IE_BS_POWER, lchan->bs_power_ctrl.current / 2);
if (lchan->meas.flags & LC_UL_M_F_L1_VALID) {
msgb_tv_fixed_put(msg, RSL_IE_L1_INFO, 2, lchan->meas.l1_info);
lchan->meas.flags &= ~LC_UL_M_F_L1_VALID;
diff --git a/src/common/scheduler.c b/src/common/scheduler.c
index 84918e31..3d780fdd 100644
--- a/src/common/scheduler.c
+++ b/src/common/scheduler.c
@@ -1237,7 +1237,7 @@ void _sched_dl_burst(struct l1sched_trx *l1t, struct trx_dl_burst_req *br)
/* BS Power reduction (in dB) per logical channel */
if (l1cs->lchan != NULL)
- br->att = l1cs->lchan->bs_power_red;
+ br->att = l1cs->lchan->bs_power_ctrl.current;
/* encrypt */
if (br->burst_len && l1cs->dl_encr_algo) {
diff --git a/src/common/tx_power.c b/src/common/tx_power.c
index 0741429f..348aba5c 100644
--- a/src/common/tx_power.c
+++ b/src/common/tx_power.c
@@ -68,7 +68,7 @@ int get_p_target_mdBm(const struct gsm_bts_trx *trx, uint8_t bs_power_red)
}
int get_p_target_mdBm_lchan(const struct gsm_lchan *lchan)
{
- return get_p_target_mdBm(lchan->ts->trx, lchan->bs_power_red);
+ return get_p_target_mdBm(lchan->ts->trx, lchan->bs_power_ctrl.current);
}
/* calculate the actual total output power required, taking into account the
@@ -134,7 +134,7 @@ int get_p_trxout_target_mdBm(const struct gsm_bts_trx *trx, uint8_t bs_power_red
}
int get_p_trxout_target_mdBm_lchan(const struct gsm_lchan *lchan)
{
- return get_p_trxout_target_mdBm(lchan->ts->trx, lchan->bs_power_red);
+ return get_p_trxout_target_mdBm(lchan->ts->trx, lchan->bs_power_ctrl.current);
}
diff --git a/src/common/vty.c b/src/common/vty.c
index f32f6cdf..5d21e584 100644
--- a/src/common/vty.c
+++ b/src/common/vty.c
@@ -1335,10 +1335,13 @@ static void lchan_dump_full_vty(struct vty *vty, const struct gsm_lchan *lchan)
lchan->state == LCHAN_S_BROKEN ? " Error reason: " : "",
lchan->state == LCHAN_S_BROKEN ? lchan->broken_reason : "",
VTY_NEWLINE);
+#if 0
+ /* TODO: print more info about MS/BS Power Control */
vty_out(vty, " BS Power: %d dBm, MS Power: %u dBm%s",
lchan->ts->trx->nominal_power - (lchan->ts->trx->max_power_red + lchan->bs_power_red),
ms_pwr_dbm(lchan->ts->trx->bts->band, lchan->ms_power_ctrl.max),
VTY_NEWLINE);
+#endif
vty_out(vty, " Channel Mode / Codec: %s%s",
gsm48_chan_mode_name(lchan->tch_mode),
VTY_NEWLINE);
@@ -1380,7 +1383,6 @@ static void lchan_dump_full_vty(struct vty *vty, const struct gsm_lchan *lchan)
if (lchan->loopback)
vty_out(vty, " RTP/PDCH Loopback Enabled%s", VTY_NEWLINE);
vty_out(vty, " Radio Link Failure Counter 'S': %d%s", lchan->s, VTY_NEWLINE);
- /* TODO: MS Power Control */
}
static void lchan_dump_short_vty(struct vty *vty, const struct gsm_lchan *lchan)
diff --git a/tests/power/Makefile.am b/tests/power/Makefile.am
index d6e1a6be..e428178a 100644
--- a/tests/power/Makefile.am
+++ b/tests/power/Makefile.am
@@ -2,8 +2,12 @@ AM_CPPFLAGS = $(all_includes) -I$(top_srcdir)/include
AM_CFLAGS = -Wall $(LIBOSMOCORE_CFLAGS) $(LIBOSMOCODEC_CFLAGS) $(LIBOSMOGSM_CFLAGS) $(LIBOSMOVTY_CFLAGS) $(LIBOSMOTRAU_CFLAGS)
LDADD = $(LIBOSMOCORE_LIBS) $(LIBOSMOCODEC_LIBS) $(LIBOSMOGSM_LIBS) $(LIBOSMOVTY_LIBS) $(LIBOSMOTRAU_LIBS)
-noinst_PROGRAMS = ms_power_loop_test
-EXTRA_DIST = ms_power_loop_test.ok ms_power_loop_test.err
+noinst_PROGRAMS = ms_power_loop_test bs_power_loop_test
+EXTRA_DIST = ms_power_loop_test.ok ms_power_loop_test.err \
+ bs_power_loop_test.ok bs_power_loop_test.err
ms_power_loop_test_SOURCES = ms_power_loop_test.c $(srcdir)/../stubs.c
ms_power_loop_test_LDADD = $(top_builddir)/src/common/libbts.a $(LIBOSMOABIS_LIBS) $(LDADD)
+
+bs_power_loop_test_SOURCES = bs_power_loop_test.c $(srcdir)/../stubs.c
+bs_power_loop_test_LDADD = $(top_builddir)/src/common/libbts.a $(LIBOSMOABIS_LIBS) $(LDADD)
diff --git a/tests/power/bs_power_loop_test.c b/tests/power/bs_power_loop_test.c
new file mode 100644
index 00000000..bc6b8154
--- /dev/null
+++ b/tests/power/bs_power_loop_test.c
@@ -0,0 +1,398 @@
+/*
+ * (C) 2020 by sysmocom - s.m.f.c. GmbH <info@sysmocom.de>
+ * Author: Vadim Yanitskiy <vyanitskiy@sysmocom.de>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+
+#include <osmocom/core/utils.h>
+#include <osmocom/core/talloc.h>
+#include <osmocom/core/application.h>
+
+#include <osmo-bts/bts.h>
+#include <osmo-bts/l1sap.h>
+#include <osmo-bts/logging.h>
+#include <osmo-bts/power_control.h>
+
+#define PWR_TEST_RXLEV_TARGET 30
+
+#define DL_MEAS_FULL(rxqual, rxlev) \
+ .rxqual_full = rxqual, \
+ .rxlev_full = rxlev
+
+#define DL_MEAS_SUB(rxqual, rxlev) \
+ .rxqual_sub = rxqual, \
+ .rxlev_sub = rxlev
+
+#define DL_MEAS_FULL_SUB(rxqual, rxlev) \
+ { DL_MEAS_FULL(rxqual, rxlev), \
+ DL_MEAS_SUB(rxqual, rxlev) }
+
+#define DL_MEAS_FULL_SUB_INV(rxqual, rxlev) \
+ { DL_MEAS_FULL(rxqual, rxlev), \
+ DL_MEAS_SUB(rxqual, rxlev), \
+ .invalid = true }
+
+enum power_test_step_type {
+ PWR_TEST_ST_IND_MEAS = 0,
+ PWR_TEST_ST_IND_DUMMY,
+ PWR_TEST_ST_SET_STATE,
+ PWR_TEST_ST_SET_PARAMS,
+ PWR_TEST_ST_ENABLE_DTXD,
+};
+
+struct power_test_step {
+ /* Instruction to be performed */
+ enum power_test_step_type type;
+ /* Instruction parameters */
+ union {
+ /* Power Control state */
+ struct lchan_power_ctrl_state state;
+ /* Power Control parameters */
+ struct bts_power_ctrl_params params;
+ /* Indicated DL measurements */
+ struct {
+ uint8_t rxqual_full;
+ uint8_t rxqual_sub;
+ uint8_t rxlev_full;
+ uint8_t rxlev_sub;
+ bool invalid;
+ } meas;
+ };
+ /* Expected Tx power reduction */
+ uint8_t exp_txred;
+};
+
+static struct gsm_bts *g_bts = NULL;
+static struct gsm_bts_trx *g_trx = NULL;
+
+static void init_test(const char *name)
+{
+ if (g_trx != NULL)
+ talloc_free(g_trx);
+ if (g_bts != NULL)
+ talloc_free(g_bts);
+
+ g_bts = talloc_zero(tall_bts_ctx, struct gsm_bts);
+ OSMO_ASSERT(g_bts != NULL);
+
+ INIT_LLIST_HEAD(&g_bts->trx_list);
+ g_trx = gsm_bts_trx_alloc(g_bts);
+ OSMO_ASSERT(g_trx != NULL);
+
+ g_bts->dl_power_ctrl.target = rxlev2dbm(PWR_TEST_RXLEV_TARGET);
+ g_bts->band = GSM_BAND_900;
+ g_bts->c0 = g_trx;
+
+ printf("\nStarting test case '%s'\n", name);
+}
+
+static void enc_meas_rep(struct gsm48_hdr *gh,
+ const unsigned int n,
+ const struct power_test_step *step)
+{
+ struct gsm48_meas_res *mr = (struct gsm48_meas_res *) gh->data;
+
+ gh->proto_discr = GSM48_PDISC_RR;
+ gh->msg_type = GSM48_MT_RR_MEAS_REP;
+
+ *mr = (struct gsm48_meas_res) {
+ .rxlev_full = step->meas.rxlev_full,
+ .rxlev_sub = step->meas.rxlev_sub,
+ .rxqual_full = step->meas.rxqual_full,
+ .rxqual_sub = step->meas.rxqual_sub,
+ /* NOTE: inversed logic (1 means invalid) */
+ .meas_valid = step->meas.invalid,
+ };
+
+ printf("#%02u %s() -> Measurement Results (%svalid): "
+ "RXLEV-FULL(%02u), RXQUAL-FULL(%u), "
+ "RXLEV-SUB(%02u), RXQUAL-SUB(%u)\n",
+ n, __func__, step->meas.invalid ? "in" : "",
+ mr->rxlev_full, mr->rxqual_full,
+ mr->rxlev_sub, mr->rxqual_sub);
+}
+
+static int exec_power_step(struct gsm_lchan *lchan,
+ const unsigned int n,
+ const struct power_test_step *step)
+{
+ struct gsm48_hdr *gh;
+ uint8_t old, new;
+ uint8_t buf[18];
+
+ gh = (struct gsm48_hdr *) buf;
+
+ switch (step->type) {
+ case PWR_TEST_ST_SET_STATE:
+ printf("#%02u %s() <- State (re)set (current %u dB, max %u dB)\n",
+ n, __func__, step->state.current, step->state.max);
+ lchan->bs_power_ctrl = step->state;
+ return 0; /* we're done */
+ case PWR_TEST_ST_SET_PARAMS:
+ printf("#%02u %s() <- Param (re)set (target %d dBm, hysteresis %u dB, "
+ "filtering is %sabled)\n",
+ n, __func__, step->params.target, step->params.hysteresis,
+ step->params.pf_algo != BTS_PF_ALGO_NONE ? "en" : "dis");
+ g_bts->dl_power_ctrl = step->params;
+ return 0; /* we're done */
+ case PWR_TEST_ST_ENABLE_DTXD:
+ printf("#%02u %s() <- Enable DTXd\n", n, __func__);
+ lchan->tch.dtx.dl_active = true;
+ return 0; /* we're done */
+ case PWR_TEST_ST_IND_DUMMY:
+ printf("#%02u %s() <- Dummy block\n", n, __func__);
+ memset(buf, 0x2b, sizeof(buf));
+ break;
+ case PWR_TEST_ST_IND_MEAS:
+ enc_meas_rep(gh, n, step);
+ break;
+ }
+
+ printf("#%02u lchan_bs_pwr_ctrl() <- UL SACCH: %s\n",
+ n, osmo_hexdump(buf, sizeof(buf)));
+
+ old = lchan->bs_power_ctrl.current;
+ lchan_bs_pwr_ctrl(lchan, gh);
+ new = lchan->bs_power_ctrl.current;
+
+ printf("#%02u lchan_bs_pwr_ctrl() -> BS power reduction: "
+ "%u -> %u (expected %u)\n",
+ n, old, new, step->exp_txred);
+
+ return new != step->exp_txred;
+}
+
+static void exec_power_test(const struct power_test_step *steps,
+ unsigned int num_steps,
+ const char *name)
+{
+ unsigned int n;
+ int rc = 0;
+
+ init_test(name);
+
+ struct gsm_lchan *lchan = &g_trx->ts[0].lchan[0];
+ for (n = 0; n < num_steps; n++)
+ rc |= exec_power_step(lchan, n, &steps[n]);
+
+ printf("Test case verdict: %s\n", rc ? "FAIL" : "SUCCESS");
+}
+
+/* Verify that the power remains constant in fixed mode. */
+static const struct power_test_step TC_fixed_mode[] = {
+ /* Initial state: 10 dB, up to 20 dB */
+ { .type = PWR_TEST_ST_SET_STATE,
+ .state = { .current = 10, .max = 2 * 10, .fixed = true } },
+
+ /* MS indicates random RxQual/RxLev values, which must be ignored */
+ { .meas = DL_MEAS_FULL_SUB(0, 63), .exp_txred = 10 },
+ { .meas = DL_MEAS_FULL_SUB(7, 0), .exp_txred = 10 },
+ { .meas = DL_MEAS_FULL_SUB(0, 30), .exp_txred = 10 },
+ { .meas = DL_MEAS_FULL_SUB(1, 30), .exp_txred = 10 },
+ { .meas = DL_MEAS_FULL_SUB(1, 50), .exp_txred = 10 },
+};
+
+/* Verify that the power remains constant if RxLev equals the target level. */
+static const struct power_test_step TC_rxlev_target[] = {
+ /* Initial state: 0 dB, up to 20 dB */
+ { .type = PWR_TEST_ST_SET_STATE,
+ .state = { .current = 0, .max = 2 * 10 } },
+
+ /* MS indicates RxLev values that match the target level */
+ { .meas = DL_MEAS_FULL_SUB(0, PWR_TEST_RXLEV_TARGET) },
+ { .meas = DL_MEAS_FULL_SUB(0, PWR_TEST_RXLEV_TARGET) },
+ { .meas = DL_MEAS_FULL_SUB(0, PWR_TEST_RXLEV_TARGET) },
+ { .meas = DL_MEAS_FULL_SUB(0, PWR_TEST_RXLEV_TARGET) },
+};
+
+/* Verify that the power is gradually reduced/increased to the
+ * minimum/maximum if the MS reports high/low RxLev values. */
+static const struct power_test_step TC_rxlev_max_min[] = {
+ /* Initial state: 0 dB, up to 20 dB */
+ { .type = PWR_TEST_ST_SET_STATE,
+ .state = { .current = 0, .max = 2 * 10 } },
+
+ /* MS indicates high RxLev values (-50 dBm) */
+ { .meas = DL_MEAS_FULL_SUB(0, 60), .exp_txred = 4 },
+ { .meas = DL_MEAS_FULL_SUB(0, 60), .exp_txred = 8 },
+ { .meas = DL_MEAS_FULL_SUB(0, 60), .exp_txred = 12 },
+ { .meas = DL_MEAS_FULL_SUB(0, 60), .exp_txred = 16 },
+ { .meas = DL_MEAS_FULL_SUB(0, 60), .exp_txred = 20 }, /* max */
+ { .meas = DL_MEAS_FULL_SUB(0, 60), .exp_txred = 20 }, /* max */
+ { .meas = DL_MEAS_FULL_SUB(0, 60), .exp_txred = 20 }, /* max */
+
+ /* MS indicates low RxLev values (-100 dBm) */
+ { .meas = DL_MEAS_FULL_SUB(0, 10), .exp_txred = 12 },
+ { .meas = DL_MEAS_FULL_SUB(0, 10), .exp_txred = 4 },
+ { .meas = DL_MEAS_FULL_SUB(0, 10), .exp_txred = 0 }, /* min */
+ { .meas = DL_MEAS_FULL_SUB(0, 10), .exp_txred = 0 }, /* min */
+ { .meas = DL_MEAS_FULL_SUB(0, 10), .exp_txred = 0 }, /* min */
+};
+
+/* Verify that the logic picks the 'SUB' values in DTXd mode. */
+static const struct power_test_step TC_dtxd_mode[] = {
+ /* Initial state: 0 dB, up to 20 dB */
+ { .type = PWR_TEST_ST_SET_STATE,
+ .state = { .current = 0, .max = 2 * 10 } },
+
+ { .meas = DL_MEAS_FULL_SUB(0, PWR_TEST_RXLEV_TARGET) },
+ { .meas = DL_MEAS_FULL_SUB(0, PWR_TEST_RXLEV_TARGET) },
+
+ { .type = PWR_TEST_ST_ENABLE_DTXD }, /* DTXd mode */
+
+ /* MS indicates target RxLev values as 'SUB', and random as 'FULL' */
+ { .meas = { DL_MEAS_FULL(7, 0), DL_MEAS_SUB(0, PWR_TEST_RXLEV_TARGET) } },
+ { .meas = { DL_MEAS_FULL(3, 30), DL_MEAS_SUB(0, PWR_TEST_RXLEV_TARGET) } },
+ { .meas = { DL_MEAS_FULL(0, 63), DL_MEAS_SUB(0, PWR_TEST_RXLEV_TARGET) } },
+};
+
+/* Verify that RxQual > 0 reduces the current attenuation value. */
+static const struct power_test_step TC_rxqual_ber[] = {
+ /* Initial state: 16 dB, up to 20 dB */
+ { .type = PWR_TEST_ST_SET_STATE,
+ .state = { .current = 16, .max = 2 * 10 } },
+
+ /* MS indicates target RxLev, and no bit errors */
+ { .meas = DL_MEAS_FULL_SUB(0, PWR_TEST_RXLEV_TARGET), .exp_txred = 16 },
+ { .meas = DL_MEAS_FULL_SUB(0, PWR_TEST_RXLEV_TARGET), .exp_txred = 16 },
+
+ /* MS indicates target RxLev, but RxQual values > 0 */
+ { .meas = DL_MEAS_FULL_SUB(7, PWR_TEST_RXLEV_TARGET), .exp_txred = 16 / 2 },
+ { .meas = DL_MEAS_FULL_SUB(4, PWR_TEST_RXLEV_TARGET), .exp_txred = 16 / 4 },
+ { .meas = DL_MEAS_FULL_SUB(1, PWR_TEST_RXLEV_TARGET), .exp_txred = 16 / 8 },
+
+ /* MS indicates target RxLev, and no bit errors anymore */
+ { .meas = DL_MEAS_FULL_SUB(0, PWR_TEST_RXLEV_TARGET), .exp_txred = 16 / 8 },
+ { .meas = DL_MEAS_FULL_SUB(0, PWR_TEST_RXLEV_TARGET), .exp_txred = 16 / 8 },
+
+ /* Reset state: 16 dB, up to 20 dB */
+ { .type = PWR_TEST_ST_SET_STATE,
+ .state = { .current = 16, .max = 2 * 10 } },
+
+ /* MS indicates target RxLev, but RxQual values > 0 again */
+ { .meas = DL_MEAS_FULL_SUB(7, PWR_TEST_RXLEV_TARGET), .exp_txred = 16 / 2 },
+ { .meas = DL_MEAS_FULL_SUB(7, PWR_TEST_RXLEV_TARGET), .exp_txred = 16 / 4 },
+ { .meas = DL_MEAS_FULL_SUB(7, PWR_TEST_RXLEV_TARGET), .exp_txred = 16 / 8 },
+ { .meas = DL_MEAS_FULL_SUB(7, PWR_TEST_RXLEV_TARGET), .exp_txred = 16 / 16 },
+ { .meas = DL_MEAS_FULL_SUB(7, PWR_TEST_RXLEV_TARGET), .exp_txred = 16 / 32 },
+};
+
+/* Verify that invalid and dummy SACCH blocks are ignored. */
+static const struct power_test_step TC_inval_dummy[] = {
+ /* Initial state: 16 dB, up to 20 dB */
+ { .type = PWR_TEST_ST_SET_STATE,
+ .state = { .current = 16, .max = 2 * 10 } },
+
+ /* MS sends invalid measurement results which must be ignored */
+ { .meas = DL_MEAS_FULL_SUB_INV(7, 63), .exp_txred = 16 },
+ { .meas = DL_MEAS_FULL_SUB_INV(0, 0), .exp_txred = 16 },
+
+ /* Let's say SMS (SAPI=3) blocks substitute some of the reports */
+ { .meas = DL_MEAS_FULL_SUB(0, PWR_TEST_RXLEV_TARGET), .exp_txred = 16 },
+ { .type = PWR_TEST_ST_IND_DUMMY, /* not a report */ .exp_txred = 16 },
+ { .meas = DL_MEAS_FULL_SUB(0, PWR_TEST_RXLEV_TARGET), .exp_txred = 16 },
+ { .type = PWR_TEST_ST_IND_DUMMY, /* not a report */ .exp_txred = 16 },
+ { .meas = DL_MEAS_FULL_SUB(0, PWR_TEST_RXLEV_TARGET), .exp_txred = 16 },
+};
+
+/* Verify that small deviations from the target do not trigger any changes. */
+static const struct power_test_step TC_rxlev_hyst[] = {
+ /* Initial state: 16 dB, up to 20 dB */
+ { .type = PWR_TEST_ST_SET_STATE,
+ .state = { .current = 12, .max = 2 * 8 } },
+
+ /* Hysteresis is not enabled, so small deviations trigger oscillations */
+ { .meas = DL_MEAS_FULL_SUB(0, PWR_TEST_RXLEV_TARGET + 1), .exp_txred = 13 },
+ { .meas = DL_MEAS_FULL_SUB(0, PWR_TEST_RXLEV_TARGET - 2), .exp_txred = 11 },
+ { .meas = DL_MEAS_FULL_SUB(0, PWR_TEST_RXLEV_TARGET + 3), .exp_txred = 14 },
+ { .meas = DL_MEAS_FULL_SUB(0, PWR_TEST_RXLEV_TARGET - 2), .exp_txred = 12 },
+
+ /* Enable hysteresis */
+ { .type = PWR_TEST_ST_SET_PARAMS,
+ .params = {
+ .target = -110 + PWR_TEST_RXLEV_TARGET,
+ .hysteresis = 3,
+ }
+ },
+
+ /* Hysteresis is enabled, so small deviations do not trigger any changes */
+ { .meas = DL_MEAS_FULL_SUB(0, PWR_TEST_RXLEV_TARGET + 1), .exp_txred = 12 },
+ { .meas = DL_MEAS_FULL_SUB(0, PWR_TEST_RXLEV_TARGET - 2), .exp_txred = 12 },
+ { .meas = DL_MEAS_FULL_SUB(0, PWR_TEST_RXLEV_TARGET + 3), .exp_txred = 12 },
+ { .meas = DL_MEAS_FULL_SUB(0, PWR_TEST_RXLEV_TARGET - 2), .exp_txred = 12 },
+};
+
+/* Verify EWMA based power filtering. */
+static const struct power_test_step TC_rxlev_pf_ewma[] = {
+ /* Initial state: 20 dB, up to 30 dB */
+ { .type = PWR_TEST_ST_SET_STATE,
+ .state = { .current = 16, .max = 2 * 15 } },
+
+ /* Enable EWMA based power filtering */
+ { .type = PWR_TEST_ST_SET_PARAMS,
+ .params = {
+ .target = -110 + PWR_TEST_RXLEV_TARGET, /* RxLev 30 */
+ .pf_algo = BTS_PF_ALGO_EWMA,
+ .pf.ewma.alpha = 50,
+ }
+ },
+
+ /* MS indicates target RxLev, power level remains constant */
+ { .meas = DL_MEAS_FULL_SUB(0, PWR_TEST_RXLEV_TARGET), .exp_txred = 16 },
+ { .meas = DL_MEAS_FULL_SUB(0, PWR_TEST_RXLEV_TARGET), .exp_txred = 16 },
+
+ /* Avg[t] = (0.5 * 26) + (0.5 * 30) = 28, so delta is 2 */
+ { .meas = DL_MEAS_FULL_SUB(0, PWR_TEST_RXLEV_TARGET - 4), .exp_txred = 14 },
+ /* Avg[t] = (0.5 * 26) + (0.5 * 28) = 27, so delta is 3 */
+ { .meas = DL_MEAS_FULL_SUB(0, PWR_TEST_RXLEV_TARGET - 4), .exp_txred = 11 },
+ /* Avg[t] = (0.5 * 35) + (0.5 * 27) = 31, so delta is 1 */
+ { .meas = DL_MEAS_FULL_SUB(0, PWR_TEST_RXLEV_TARGET + 5), .exp_txred = 12 },
+ /* Avg[t] = (0.5 * 35) + (0.5 * 31) = 33, so delta is 3 */
+ { .meas = DL_MEAS_FULL_SUB(0, PWR_TEST_RXLEV_TARGET + 5), .exp_txred = 15 },
+};
+
+int main(int argc, char **argv)
+{
+ printf("Testing BS Power loop...\n");
+
+ tall_bts_ctx = talloc_named_const(NULL, 1, "OsmoBTS context");
+ msgb_talloc_ctx_init(tall_bts_ctx, 0);
+
+ osmo_init_logging2(tall_bts_ctx, &bts_log_info);
+ osmo_stderr_target->categories[DLOOP].loglevel = LOGL_DEBUG;
+ osmo_stderr_target->categories[DL1C].loglevel = LOGL_DEBUG;
+ log_set_print_filename(osmo_stderr_target, 0);
+ log_set_use_color(osmo_stderr_target, 0);
+
+#define exec_test(test) \
+ exec_power_test(test, ARRAY_SIZE(test), #test)
+
+ exec_test(TC_fixed_mode);
+ exec_test(TC_rxlev_target);
+ exec_test(TC_rxlev_max_min); /* FIXME */
+
+ exec_test(TC_dtxd_mode);
+ exec_test(TC_rxqual_ber);
+ exec_test(TC_inval_dummy);
+
+ exec_test(TC_rxlev_hyst);
+ exec_test(TC_rxlev_pf_ewma);
+
+ return 0;
+}
diff --git a/tests/power/bs_power_loop_test.err b/tests/power/bs_power_loop_test.err
new file mode 100644
index 00000000..44d996e5
--- /dev/null
+++ b/tests/power/bs_power_loop_test.err
@@ -0,0 +1,102 @@
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(30), RXQUAL-FULL(0), RXLEV-SUB(30), RXQUAL-SUB(0), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Keeping Downlink attenuation at 0 dB (maximum 20 dB, target -80 dBm, delta 0 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(30), RXQUAL-FULL(0), RXLEV-SUB(30), RXQUAL-SUB(0), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Keeping Downlink attenuation at 0 dB (maximum 20 dB, target -80 dBm, delta 0 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(30), RXQUAL-FULL(0), RXLEV-SUB(30), RXQUAL-SUB(0), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Keeping Downlink attenuation at 0 dB (maximum 20 dB, target -80 dBm, delta 0 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(30), RXQUAL-FULL(0), RXLEV-SUB(30), RXQUAL-SUB(0), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Keeping Downlink attenuation at 0 dB (maximum 20 dB, target -80 dBm, delta 0 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(60), RXQUAL-FULL(0), RXLEV-SUB(60), RXQUAL-SUB(0), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Changing Downlink attenuation: 0 -> 8 dB (maximum 20 dB, target -80 dBm, delta -8 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(60), RXQUAL-FULL(0), RXLEV-SUB(60), RXQUAL-SUB(0), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Changing Downlink attenuation: 8 -> 16 dB (maximum 20 dB, target -80 dBm, delta -8 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(60), RXQUAL-FULL(0), RXLEV-SUB(60), RXQUAL-SUB(0), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Changing Downlink attenuation: 16 -> 20 dB (maximum 20 dB, target -80 dBm, delta -8 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(60), RXQUAL-FULL(0), RXLEV-SUB(60), RXQUAL-SUB(0), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Keeping Downlink attenuation at 20 dB (maximum 20 dB, target -80 dBm, delta -8 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(60), RXQUAL-FULL(0), RXLEV-SUB(60), RXQUAL-SUB(0), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Keeping Downlink attenuation at 20 dB (maximum 20 dB, target -80 dBm, delta -8 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(60), RXQUAL-FULL(0), RXLEV-SUB(60), RXQUAL-SUB(0), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Keeping Downlink attenuation at 20 dB (maximum 20 dB, target -80 dBm, delta -8 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(60), RXQUAL-FULL(0), RXLEV-SUB(60), RXQUAL-SUB(0), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Keeping Downlink attenuation at 20 dB (maximum 20 dB, target -80 dBm, delta -8 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(10), RXQUAL-FULL(0), RXLEV-SUB(10), RXQUAL-SUB(0), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Changing Downlink attenuation: 20 -> 16 dB (maximum 20 dB, target -80 dBm, delta 4 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(10), RXQUAL-FULL(0), RXLEV-SUB(10), RXQUAL-SUB(0), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Changing Downlink attenuation: 16 -> 12 dB (maximum 20 dB, target -80 dBm, delta 4 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(10), RXQUAL-FULL(0), RXLEV-SUB(10), RXQUAL-SUB(0), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Changing Downlink attenuation: 12 -> 8 dB (maximum 20 dB, target -80 dBm, delta 4 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(10), RXQUAL-FULL(0), RXLEV-SUB(10), RXQUAL-SUB(0), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Changing Downlink attenuation: 8 -> 4 dB (maximum 20 dB, target -80 dBm, delta 4 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(10), RXQUAL-FULL(0), RXLEV-SUB(10), RXQUAL-SUB(0), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Changing Downlink attenuation: 4 -> 0 dB (maximum 20 dB, target -80 dBm, delta 4 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(30), RXQUAL-FULL(0), RXLEV-SUB(30), RXQUAL-SUB(0), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Keeping Downlink attenuation at 0 dB (maximum 20 dB, target -80 dBm, delta 0 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(30), RXQUAL-FULL(0), RXLEV-SUB(30), RXQUAL-SUB(0), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Keeping Downlink attenuation at 0 dB (maximum 20 dB, target -80 dBm, delta 0 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(00), RXQUAL-FULL(7), RXLEV-SUB(30), RXQUAL-SUB(0), DTx is enabled => using SUB
+(bts=0,trx=0,ts=0,ss=0) Keeping Downlink attenuation at 0 dB (maximum 20 dB, target -80 dBm, delta 0 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(30), RXQUAL-FULL(3), RXLEV-SUB(30), RXQUAL-SUB(0), DTx is enabled => using SUB
+(bts=0,trx=0,ts=0,ss=0) Keeping Downlink attenuation at 0 dB (maximum 20 dB, target -80 dBm, delta 0 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(63), RXQUAL-FULL(0), RXLEV-SUB(30), RXQUAL-SUB(0), DTx is enabled => using SUB
+(bts=0,trx=0,ts=0,ss=0) Keeping Downlink attenuation at 0 dB (maximum 20 dB, target -80 dBm, delta 0 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(30), RXQUAL-FULL(0), RXLEV-SUB(30), RXQUAL-SUB(0), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Keeping Downlink attenuation at 16 dB (maximum 20 dB, target -80 dBm, delta 0 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(30), RXQUAL-FULL(0), RXLEV-SUB(30), RXQUAL-SUB(0), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Keeping Downlink attenuation at 16 dB (maximum 20 dB, target -80 dBm, delta 0 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(30), RXQUAL-FULL(7), RXLEV-SUB(30), RXQUAL-SUB(7), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Reducing Downlink attenuation by half: 16 -> 8 dB due to RXQUAL 7 > 0
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(30), RXQUAL-FULL(4), RXLEV-SUB(30), RXQUAL-SUB(4), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Reducing Downlink attenuation by half: 8 -> 4 dB due to RXQUAL 4 > 0
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(30), RXQUAL-FULL(1), RXLEV-SUB(30), RXQUAL-SUB(1), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Reducing Downlink attenuation by half: 4 -> 2 dB due to RXQUAL 1 > 0
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(30), RXQUAL-FULL(0), RXLEV-SUB(30), RXQUAL-SUB(0), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Keeping Downlink attenuation at 2 dB (maximum 20 dB, target -80 dBm, delta 0 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(30), RXQUAL-FULL(0), RXLEV-SUB(30), RXQUAL-SUB(0), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Keeping Downlink attenuation at 2 dB (maximum 20 dB, target -80 dBm, delta 0 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(30), RXQUAL-FULL(7), RXLEV-SUB(30), RXQUAL-SUB(7), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Reducing Downlink attenuation by half: 16 -> 8 dB due to RXQUAL 7 > 0
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(30), RXQUAL-FULL(7), RXLEV-SUB(30), RXQUAL-SUB(7), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Reducing Downlink attenuation by half: 8 -> 4 dB due to RXQUAL 7 > 0
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(30), RXQUAL-FULL(7), RXLEV-SUB(30), RXQUAL-SUB(7), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Reducing Downlink attenuation by half: 4 -> 2 dB due to RXQUAL 7 > 0
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(30), RXQUAL-FULL(7), RXLEV-SUB(30), RXQUAL-SUB(7), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Reducing Downlink attenuation by half: 2 -> 1 dB due to RXQUAL 7 > 0
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(30), RXQUAL-FULL(7), RXLEV-SUB(30), RXQUAL-SUB(7), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Reducing Downlink attenuation by half: 1 -> 0 dB due to RXQUAL 7 > 0
+(bts=0,trx=0,ts=0,ss=0) The measurement results are not valid
+(bts=0,trx=0,ts=0,ss=0) The measurement results are not valid
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(30), RXQUAL-FULL(0), RXLEV-SUB(30), RXQUAL-SUB(0), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Keeping Downlink attenuation at 16 dB (maximum 20 dB, target -80 dBm, delta 0 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(30), RXQUAL-FULL(0), RXLEV-SUB(30), RXQUAL-SUB(0), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Keeping Downlink attenuation at 16 dB (maximum 20 dB, target -80 dBm, delta 0 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(30), RXQUAL-FULL(0), RXLEV-SUB(30), RXQUAL-SUB(0), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Keeping Downlink attenuation at 16 dB (maximum 20 dB, target -80 dBm, delta 0 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(31), RXQUAL-FULL(0), RXLEV-SUB(31), RXQUAL-SUB(0), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Changing Downlink attenuation: 12 -> 13 dB (maximum 16 dB, target -80 dBm, delta -1 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(28), RXQUAL-FULL(0), RXLEV-SUB(28), RXQUAL-SUB(0), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Changing Downlink attenuation: 13 -> 11 dB (maximum 16 dB, target -80 dBm, delta 2 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(33), RXQUAL-FULL(0), RXLEV-SUB(33), RXQUAL-SUB(0), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Changing Downlink attenuation: 11 -> 14 dB (maximum 16 dB, target -80 dBm, delta -3 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(28), RXQUAL-FULL(0), RXLEV-SUB(28), RXQUAL-SUB(0), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Changing Downlink attenuation: 14 -> 12 dB (maximum 16 dB, target -80 dBm, delta 2 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(31), RXQUAL-FULL(0), RXLEV-SUB(31), RXQUAL-SUB(0), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Keeping Downlink attenuation at 12 dB (maximum 16 dB, target -80 dBm, delta 0 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(28), RXQUAL-FULL(0), RXLEV-SUB(28), RXQUAL-SUB(0), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Keeping Downlink attenuation at 12 dB (maximum 16 dB, target -80 dBm, delta 0 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(33), RXQUAL-FULL(0), RXLEV-SUB(33), RXQUAL-SUB(0), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Keeping Downlink attenuation at 12 dB (maximum 16 dB, target -80 dBm, delta 0 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(28), RXQUAL-FULL(0), RXLEV-SUB(28), RXQUAL-SUB(0), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Keeping Downlink attenuation at 12 dB (maximum 16 dB, target -80 dBm, delta 0 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(30), RXQUAL-FULL(0), RXLEV-SUB(30), RXQUAL-SUB(0), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Keeping Downlink attenuation at 16 dB (maximum 30 dB, target -80 dBm, delta 0 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(30), RXQUAL-FULL(0), RXLEV-SUB(30), RXQUAL-SUB(0), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Keeping Downlink attenuation at 16 dB (maximum 30 dB, target -80 dBm, delta 0 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(26), RXQUAL-FULL(0), RXLEV-SUB(26), RXQUAL-SUB(0), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Changing Downlink attenuation: 16 -> 14 dB (maximum 30 dB, target -80 dBm, delta 2 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(26), RXQUAL-FULL(0), RXLEV-SUB(26), RXQUAL-SUB(0), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Changing Downlink attenuation: 14 -> 11 dB (maximum 30 dB, target -80 dBm, delta 3 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(35), RXQUAL-FULL(0), RXLEV-SUB(35), RXQUAL-SUB(0), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Changing Downlink attenuation: 11 -> 12 dB (maximum 30 dB, target -80 dBm, delta -1 dB)
+(bts=0,trx=0,ts=0,ss=0) Rx DL Measurement Report: RXLEV-FULL(35), RXQUAL-FULL(0), RXLEV-SUB(35), RXQUAL-SUB(0), DTx is disabled => using FULL
+(bts=0,trx=0,ts=0,ss=0) Changing Downlink attenuation: 12 -> 15 dB (maximum 30 dB, target -80 dBm, delta -3 dB)
diff --git a/tests/power/bs_power_loop_test.ok b/tests/power/bs_power_loop_test.ok
new file mode 100644
index 00000000..fe2eb78d
--- /dev/null
+++ b/tests/power/bs_power_loop_test.ok
@@ -0,0 +1,214 @@
+Testing BS Power loop...
+
+Starting test case 'TC_fixed_mode'
+#00 exec_power_step() <- State (re)set (current 10 dB, max 20 dB)
+#01 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(63), RXQUAL-FULL(0), RXLEV-SUB(63), RXQUAL-SUB(0)
+#01 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 3f 3f 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#01 lchan_bs_pwr_ctrl() -> BS power reduction: 10 -> 10 (expected 10)
+#02 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(00), RXQUAL-FULL(7), RXLEV-SUB(00), RXQUAL-SUB(7)
+#02 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 00 00 7e 00 00 00 00 00 00 00 00 00 00 00 00 00
+#02 lchan_bs_pwr_ctrl() -> BS power reduction: 10 -> 10 (expected 10)
+#03 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(30), RXQUAL-FULL(0), RXLEV-SUB(30), RXQUAL-SUB(0)
+#03 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 1e 1e 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#03 lchan_bs_pwr_ctrl() -> BS power reduction: 10 -> 10 (expected 10)
+#04 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(30), RXQUAL-FULL(1), RXLEV-SUB(30), RXQUAL-SUB(1)
+#04 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 1e 1e 12 00 00 00 00 00 00 00 00 00 00 00 00 00
+#04 lchan_bs_pwr_ctrl() -> BS power reduction: 10 -> 10 (expected 10)
+#05 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(50), RXQUAL-FULL(1), RXLEV-SUB(50), RXQUAL-SUB(1)
+#05 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 32 32 12 00 00 00 00 00 00 00 00 00 00 00 00 00
+#05 lchan_bs_pwr_ctrl() -> BS power reduction: 10 -> 10 (expected 10)
+Test case verdict: SUCCESS
+
+Starting test case 'TC_rxlev_target'
+#00 exec_power_step() <- State (re)set (current 0 dB, max 20 dB)
+#01 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(30), RXQUAL-FULL(0), RXLEV-SUB(30), RXQUAL-SUB(0)
+#01 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 1e 1e 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#01 lchan_bs_pwr_ctrl() -> BS power reduction: 0 -> 0 (expected 0)
+#02 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(30), RXQUAL-FULL(0), RXLEV-SUB(30), RXQUAL-SUB(0)
+#02 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 1e 1e 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#02 lchan_bs_pwr_ctrl() -> BS power reduction: 0 -> 0 (expected 0)
+#03 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(30), RXQUAL-FULL(0), RXLEV-SUB(30), RXQUAL-SUB(0)
+#03 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 1e 1e 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#03 lchan_bs_pwr_ctrl() -> BS power reduction: 0 -> 0 (expected 0)
+#04 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(30), RXQUAL-FULL(0), RXLEV-SUB(30), RXQUAL-SUB(0)
+#04 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 1e 1e 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#04 lchan_bs_pwr_ctrl() -> BS power reduction: 0 -> 0 (expected 0)
+Test case verdict: SUCCESS
+
+Starting test case 'TC_rxlev_max_min'
+#00 exec_power_step() <- State (re)set (current 0 dB, max 20 dB)
+#01 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(60), RXQUAL-FULL(0), RXLEV-SUB(60), RXQUAL-SUB(0)
+#01 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 3c 3c 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#01 lchan_bs_pwr_ctrl() -> BS power reduction: 0 -> 8 (expected 4)
+#02 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(60), RXQUAL-FULL(0), RXLEV-SUB(60), RXQUAL-SUB(0)
+#02 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 3c 3c 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#02 lchan_bs_pwr_ctrl() -> BS power reduction: 8 -> 16 (expected 8)
+#03 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(60), RXQUAL-FULL(0), RXLEV-SUB(60), RXQUAL-SUB(0)
+#03 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 3c 3c 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#03 lchan_bs_pwr_ctrl() -> BS power reduction: 16 -> 20 (expected 12)
+#04 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(60), RXQUAL-FULL(0), RXLEV-SUB(60), RXQUAL-SUB(0)
+#04 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 3c 3c 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#04 lchan_bs_pwr_ctrl() -> BS power reduction: 20 -> 20 (expected 16)
+#05 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(60), RXQUAL-FULL(0), RXLEV-SUB(60), RXQUAL-SUB(0)
+#05 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 3c 3c 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#05 lchan_bs_pwr_ctrl() -> BS power reduction: 20 -> 20 (expected 20)
+#06 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(60), RXQUAL-FULL(0), RXLEV-SUB(60), RXQUAL-SUB(0)
+#06 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 3c 3c 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#06 lchan_bs_pwr_ctrl() -> BS power reduction: 20 -> 20 (expected 20)
+#07 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(60), RXQUAL-FULL(0), RXLEV-SUB(60), RXQUAL-SUB(0)
+#07 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 3c 3c 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#07 lchan_bs_pwr_ctrl() -> BS power reduction: 20 -> 20 (expected 20)
+#08 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(10), RXQUAL-FULL(0), RXLEV-SUB(10), RXQUAL-SUB(0)
+#08 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 0a 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#08 lchan_bs_pwr_ctrl() -> BS power reduction: 20 -> 16 (expected 12)
+#09 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(10), RXQUAL-FULL(0), RXLEV-SUB(10), RXQUAL-SUB(0)
+#09 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 0a 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#09 lchan_bs_pwr_ctrl() -> BS power reduction: 16 -> 12 (expected 4)
+#10 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(10), RXQUAL-FULL(0), RXLEV-SUB(10), RXQUAL-SUB(0)
+#10 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 0a 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#10 lchan_bs_pwr_ctrl() -> BS power reduction: 12 -> 8 (expected 0)
+#11 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(10), RXQUAL-FULL(0), RXLEV-SUB(10), RXQUAL-SUB(0)
+#11 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 0a 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#11 lchan_bs_pwr_ctrl() -> BS power reduction: 8 -> 4 (expected 0)
+#12 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(10), RXQUAL-FULL(0), RXLEV-SUB(10), RXQUAL-SUB(0)
+#12 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 0a 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#12 lchan_bs_pwr_ctrl() -> BS power reduction: 4 -> 0 (expected 0)
+Test case verdict: FAIL
+
+Starting test case 'TC_dtxd_mode'
+#00 exec_power_step() <- State (re)set (current 0 dB, max 20 dB)
+#01 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(30), RXQUAL-FULL(0), RXLEV-SUB(30), RXQUAL-SUB(0)
+#01 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 1e 1e 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#01 lchan_bs_pwr_ctrl() -> BS power reduction: 0 -> 0 (expected 0)
+#02 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(30), RXQUAL-FULL(0), RXLEV-SUB(30), RXQUAL-SUB(0)
+#02 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 1e 1e 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#02 lchan_bs_pwr_ctrl() -> BS power reduction: 0 -> 0 (expected 0)
+#03 exec_power_step() <- Enable DTXd
+#04 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(00), RXQUAL-FULL(7), RXLEV-SUB(30), RXQUAL-SUB(0)
+#04 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 00 1e 70 00 00 00 00 00 00 00 00 00 00 00 00 00
+#04 lchan_bs_pwr_ctrl() -> BS power reduction: 0 -> 0 (expected 0)
+#05 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(30), RXQUAL-FULL(3), RXLEV-SUB(30), RXQUAL-SUB(0)
+#05 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 1e 1e 30 00 00 00 00 00 00 00 00 00 00 00 00 00
+#05 lchan_bs_pwr_ctrl() -> BS power reduction: 0 -> 0 (expected 0)
+#06 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(63), RXQUAL-FULL(0), RXLEV-SUB(30), RXQUAL-SUB(0)
+#06 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 3f 1e 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#06 lchan_bs_pwr_ctrl() -> BS power reduction: 0 -> 0 (expected 0)
+Test case verdict: SUCCESS
+
+Starting test case 'TC_rxqual_ber'
+#00 exec_power_step() <- State (re)set (current 16 dB, max 20 dB)
+#01 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(30), RXQUAL-FULL(0), RXLEV-SUB(30), RXQUAL-SUB(0)
+#01 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 1e 1e 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#01 lchan_bs_pwr_ctrl() -> BS power reduction: 16 -> 16 (expected 16)
+#02 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(30), RXQUAL-FULL(0), RXLEV-SUB(30), RXQUAL-SUB(0)
+#02 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 1e 1e 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#02 lchan_bs_pwr_ctrl() -> BS power reduction: 16 -> 16 (expected 16)
+#03 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(30), RXQUAL-FULL(7), RXLEV-SUB(30), RXQUAL-SUB(7)
+#03 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 1e 1e 7e 00 00 00 00 00 00 00 00 00 00 00 00 00
+#03 lchan_bs_pwr_ctrl() -> BS power reduction: 16 -> 8 (expected 8)
+#04 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(30), RXQUAL-FULL(4), RXLEV-SUB(30), RXQUAL-SUB(4)
+#04 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 1e 1e 48 00 00 00 00 00 00 00 00 00 00 00 00 00
+#04 lchan_bs_pwr_ctrl() -> BS power reduction: 8 -> 4 (expected 4)
+#05 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(30), RXQUAL-FULL(1), RXLEV-SUB(30), RXQUAL-SUB(1)
+#05 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 1e 1e 12 00 00 00 00 00 00 00 00 00 00 00 00 00
+#05 lchan_bs_pwr_ctrl() -> BS power reduction: 4 -> 2 (expected 2)
+#06 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(30), RXQUAL-FULL(0), RXLEV-SUB(30), RXQUAL-SUB(0)
+#06 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 1e 1e 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#06 lchan_bs_pwr_ctrl() -> BS power reduction: 2 -> 2 (expected 2)
+#07 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(30), RXQUAL-FULL(0), RXLEV-SUB(30), RXQUAL-SUB(0)
+#07 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 1e 1e 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#07 lchan_bs_pwr_ctrl() -> BS power reduction: 2 -> 2 (expected 2)
+#08 exec_power_step() <- State (re)set (current 16 dB, max 20 dB)
+#09 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(30), RXQUAL-FULL(7), RXLEV-SUB(30), RXQUAL-SUB(7)
+#09 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 1e 1e 7e 00 00 00 00 00 00 00 00 00 00 00 00 00
+#09 lchan_bs_pwr_ctrl() -> BS power reduction: 16 -> 8 (expected 8)
+#10 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(30), RXQUAL-FULL(7), RXLEV-SUB(30), RXQUAL-SUB(7)
+#10 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 1e 1e 7e 00 00 00 00 00 00 00 00 00 00 00 00 00
+#10 lchan_bs_pwr_ctrl() -> BS power reduction: 8 -> 4 (expected 4)
+#11 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(30), RXQUAL-FULL(7), RXLEV-SUB(30), RXQUAL-SUB(7)
+#11 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 1e 1e 7e 00 00 00 00 00 00 00 00 00 00 00 00 00
+#11 lchan_bs_pwr_ctrl() -> BS power reduction: 4 -> 2 (expected 2)
+#12 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(30), RXQUAL-FULL(7), RXLEV-SUB(30), RXQUAL-SUB(7)
+#12 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 1e 1e 7e 00 00 00 00 00 00 00 00 00 00 00 00 00
+#12 lchan_bs_pwr_ctrl() -> BS power reduction: 2 -> 1 (expected 1)
+#13 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(30), RXQUAL-FULL(7), RXLEV-SUB(30), RXQUAL-SUB(7)
+#13 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 1e 1e 7e 00 00 00 00 00 00 00 00 00 00 00 00 00
+#13 lchan_bs_pwr_ctrl() -> BS power reduction: 1 -> 0 (expected 0)
+Test case verdict: SUCCESS
+
+Starting test case 'TC_inval_dummy'
+#00 exec_power_step() <- State (re)set (current 16 dB, max 20 dB)
+#01 enc_meas_rep() -> Measurement Results (invalid): RXLEV-FULL(63), RXQUAL-FULL(7), RXLEV-SUB(63), RXQUAL-SUB(7)
+#01 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 3f 7f 7e 00 00 00 00 00 00 00 00 00 00 00 00 00
+#01 lchan_bs_pwr_ctrl() -> BS power reduction: 16 -> 16 (expected 16)
+#02 enc_meas_rep() -> Measurement Results (invalid): RXLEV-FULL(00), RXQUAL-FULL(0), RXLEV-SUB(00), RXQUAL-SUB(0)
+#02 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 00 40 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#02 lchan_bs_pwr_ctrl() -> BS power reduction: 16 -> 16 (expected 16)
+#03 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(30), RXQUAL-FULL(0), RXLEV-SUB(30), RXQUAL-SUB(0)
+#03 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 1e 1e 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#03 lchan_bs_pwr_ctrl() -> BS power reduction: 16 -> 16 (expected 16)
+#04 exec_power_step() <- Dummy block
+#04 lchan_bs_pwr_ctrl() <- UL SACCH: 2b 2b 2b 2b 2b 2b 2b 2b 2b 2b 2b 2b 2b 2b 2b 2b 2b 2b
+#04 lchan_bs_pwr_ctrl() -> BS power reduction: 16 -> 16 (expected 16)
+#05 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(30), RXQUAL-FULL(0), RXLEV-SUB(30), RXQUAL-SUB(0)
+#05 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 1e 1e 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#05 lchan_bs_pwr_ctrl() -> BS power reduction: 16 -> 16 (expected 16)
+#06 exec_power_step() <- Dummy block
+#06 lchan_bs_pwr_ctrl() <- UL SACCH: 2b 2b 2b 2b 2b 2b 2b 2b 2b 2b 2b 2b 2b 2b 2b 2b 2b 2b
+#06 lchan_bs_pwr_ctrl() -> BS power reduction: 16 -> 16 (expected 16)
+#07 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(30), RXQUAL-FULL(0), RXLEV-SUB(30), RXQUAL-SUB(0)
+#07 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 1e 1e 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#07 lchan_bs_pwr_ctrl() -> BS power reduction: 16 -> 16 (expected 16)
+Test case verdict: SUCCESS
+
+Starting test case 'TC_rxlev_hyst'
+#00 exec_power_step() <- State (re)set (current 12 dB, max 16 dB)
+#01 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(31), RXQUAL-FULL(0), RXLEV-SUB(31), RXQUAL-SUB(0)
+#01 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 1f 1f 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#01 lchan_bs_pwr_ctrl() -> BS power reduction: 12 -> 13 (expected 13)
+#02 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(28), RXQUAL-FULL(0), RXLEV-SUB(28), RXQUAL-SUB(0)
+#02 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 1c 1c 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#02 lchan_bs_pwr_ctrl() -> BS power reduction: 13 -> 11 (expected 11)
+#03 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(33), RXQUAL-FULL(0), RXLEV-SUB(33), RXQUAL-SUB(0)
+#03 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 21 21 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#03 lchan_bs_pwr_ctrl() -> BS power reduction: 11 -> 14 (expected 14)
+#04 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(28), RXQUAL-FULL(0), RXLEV-SUB(28), RXQUAL-SUB(0)
+#04 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 1c 1c 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#04 lchan_bs_pwr_ctrl() -> BS power reduction: 14 -> 12 (expected 12)
+#05 exec_power_step() <- Param (re)set (target -80 dBm, hysteresis 3 dB, filtering is disabled)
+#06 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(31), RXQUAL-FULL(0), RXLEV-SUB(31), RXQUAL-SUB(0)
+#06 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 1f 1f 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#06 lchan_bs_pwr_ctrl() -> BS power reduction: 12 -> 12 (expected 12)
+#07 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(28), RXQUAL-FULL(0), RXLEV-SUB(28), RXQUAL-SUB(0)
+#07 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 1c 1c 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#07 lchan_bs_pwr_ctrl() -> BS power reduction: 12 -> 12 (expected 12)
+#08 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(33), RXQUAL-FULL(0), RXLEV-SUB(33), RXQUAL-SUB(0)
+#08 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 21 21 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#08 lchan_bs_pwr_ctrl() -> BS power reduction: 12 -> 12 (expected 12)
+#09 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(28), RXQUAL-FULL(0), RXLEV-SUB(28), RXQUAL-SUB(0)
+#09 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 1c 1c 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#09 lchan_bs_pwr_ctrl() -> BS power reduction: 12 -> 12 (expected 12)
+Test case verdict: SUCCESS
+
+Starting test case 'TC_rxlev_pf_ewma'
+#00 exec_power_step() <- State (re)set (current 16 dB, max 30 dB)
+#01 exec_power_step() <- Param (re)set (target -80 dBm, hysteresis 0 dB, filtering is enabled)
+#02 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(30), RXQUAL-FULL(0), RXLEV-SUB(30), RXQUAL-SUB(0)
+#02 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 1e 1e 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#02 lchan_bs_pwr_ctrl() -> BS power reduction: 16 -> 16 (expected 16)
+#03 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(30), RXQUAL-FULL(0), RXLEV-SUB(30), RXQUAL-SUB(0)
+#03 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 1e 1e 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#03 lchan_bs_pwr_ctrl() -> BS power reduction: 16 -> 16 (expected 16)
+#04 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(26), RXQUAL-FULL(0), RXLEV-SUB(26), RXQUAL-SUB(0)
+#04 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 1a 1a 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#04 lchan_bs_pwr_ctrl() -> BS power reduction: 16 -> 14 (expected 14)
+#05 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(26), RXQUAL-FULL(0), RXLEV-SUB(26), RXQUAL-SUB(0)
+#05 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 1a 1a 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#05 lchan_bs_pwr_ctrl() -> BS power reduction: 14 -> 11 (expected 11)
+#06 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(35), RXQUAL-FULL(0), RXLEV-SUB(35), RXQUAL-SUB(0)
+#06 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 23 23 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#06 lchan_bs_pwr_ctrl() -> BS power reduction: 11 -> 12 (expected 12)
+#07 enc_meas_rep() -> Measurement Results (valid): RXLEV-FULL(35), RXQUAL-FULL(0), RXLEV-SUB(35), RXQUAL-SUB(0)
+#07 lchan_bs_pwr_ctrl() <- UL SACCH: 06 15 23 23 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+#07 lchan_bs_pwr_ctrl() -> BS power reduction: 12 -> 15 (expected 15)
+Test case verdict: SUCCESS
diff --git a/tests/testsuite.at b/tests/testsuite.at
index 0590580f..ba5a409b 100644
--- a/tests/testsuite.at
+++ b/tests/testsuite.at
@@ -38,6 +38,13 @@ cat $abs_srcdir/power/ms_power_loop_test.err > experr
AT_CHECK([$abs_top_builddir/tests/power/ms_power_loop_test], [], [expout], [experr])
AT_CLEANUP
+AT_SETUP([bs_power_loop])
+AT_KEYWORDS([power])
+cat $abs_srcdir/power/bs_power_loop_test.ok > expout
+cat $abs_srcdir/power/bs_power_loop_test.err > experr
+AT_CHECK([$abs_top_builddir/tests/power/bs_power_loop_test], [], [expout], [experr])
+AT_CLEANUP
+
AT_SETUP([tx_power])
AT_KEYWORDS([tx_power])
cat $abs_srcdir/tx_power/tx_power_test.ok > expout