diff options
Diffstat (limited to 'src/osmo-bsc/lchan_select.c')
-rw-r--r-- | src/osmo-bsc/lchan_select.c | 436 |
1 files changed, 315 insertions, 121 deletions
diff --git a/src/osmo-bsc/lchan_select.c b/src/osmo-bsc/lchan_select.c index 6d3caaced..2388e2449 100644 --- a/src/osmo-bsc/lchan_select.c +++ b/src/osmo-bsc/lchan_select.c @@ -2,7 +2,7 @@ * * (C) 2008 by Harald Welte <laforge@gnumonks.org> * (C) 2008, 2009 by Holger Hans Peter Freyther <zecke@selfish.org> - * (C) 2018 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de> + * (C) 2018-2022 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de> * * All Rights Reserved * @@ -21,6 +21,8 @@ * */ +#include <stdlib.h> + #include <osmocom/bsc/debug.h> #include <osmocom/bsc/gsm_data.h> @@ -30,148 +32,332 @@ #include <osmocom/bsc/lchan_select.h> #include <osmocom/bsc/bts.h> -static struct gsm_lchan * -_lc_find_trx(struct gsm_bts_trx *trx, enum gsm_phys_chan_config pchan, - enum gsm_phys_chan_config as_pchan) +struct lchan_select_ts_list { + struct gsm_bts_trx_ts **list; + unsigned int num; +}; + +const struct value_string lchan_select_reason_names[] = { + OSMO_VALUE_STRING(SELECT_FOR_MS_CHAN_REQ), + OSMO_VALUE_STRING(SELECT_FOR_ASSIGNMENT), + OSMO_VALUE_STRING(SELECT_FOR_HANDOVER), + {0, NULL} +}; + +static struct gsm_lchan *pick_better_lchan(struct gsm_lchan *a, struct gsm_lchan *b) +{ + if (!a) + return b; + if (!b) + return a; + /* comparing negative dBm values: smaller value means less interference. */ + if (b->interf_dbm < a->interf_dbm) + return b; + return a; +} + +static struct gsm_lchan *_lc_find(struct lchan_select_ts_list *ts_list, + enum gsm_phys_chan_config pchan, + enum gsm_phys_chan_config as_pchan, + bool allow_pchan_switch, bool log) { struct gsm_lchan *lchan; - struct gsm_bts_trx_ts *ts; - int j, start, stop, dir; + struct gsm_lchan *found_lchan = NULL; -#define LOGPLCHANALLOC(fmt, args...) \ - LOGP(DRLL, LOGL_DEBUG, "looking for lchan %s%s%s: " fmt, \ +#define LOGPLCHANALLOC(fmt, args...) do { \ + if (log) \ + LOGP(DRLL, LOGL_DEBUG, "looking for lchan %s%s%s%s: " fmt, \ gsm_pchan_name(pchan), \ pchan == as_pchan ? "" : " as ", \ - pchan == as_pchan ? "" : gsm_pchan_name(as_pchan), ## args) + pchan == as_pchan ? "" : gsm_pchan_name(as_pchan), \ + ((pchan != as_pchan) && !allow_pchan_switch) ? " without pchan switch" : "", \ + ## args); \ + } while (0) - if (!trx_is_usable(trx)) { - LOGPLCHANALLOC("%s trx not usable\n", gsm_trx_name(trx)); - return NULL; - } + for (unsigned int tn = 0; tn < ts_list->num; tn++) { + struct gsm_bts_trx_ts *ts = ts_list->list[tn]; + int lchans_as_pchan; - if (trx->bts->chan_alloc_reverse) { - /* check TS 7..0 */ - start = 7; - stop = -1; - dir = -1; - } else { - /* check TS 0..7 */ - start = 0; - stop = 8; - dir = 1; - } - - for (j = start; j != stop; j += dir) { - ts = &trx->ts[j]; - if (!ts_is_usable(ts)) - continue; /* The caller first selects what kind of TS to search in, e.g. looking for exact - * GSM_PCHAN_TCH_F, or maybe among dynamic GSM_PCHAN_TCH_F_TCH_H_PDCH... */ + * GSM_PCHAN_TCH_F, or maybe among dynamic GSM_PCHAN_OSMO_DYN... */ if (ts->pchan_on_init != pchan) { LOGPLCHANALLOC("%s is != %s\n", gsm_ts_and_pchan_name(ts), gsm_pchan_name(pchan)); continue; } /* Next, is this timeslot in or can it be switched to the pchan we want to use it for? */ - if (!ts_usable_as_pchan(ts, as_pchan)) { - LOGPLCHANALLOC("%s is not usable as %s\n", gsm_ts_and_pchan_name(ts), - gsm_pchan_name(as_pchan)); + if (!ts_usable_as_pchan(ts, as_pchan, allow_pchan_switch)) { + LOGPLCHANALLOC("%s is not usable as %s%s\n", gsm_ts_and_pchan_name(ts), + gsm_pchan_name(as_pchan), + allow_pchan_switch ? "" : " without pchan switch"); continue; } /* TS is (going to be) in desired pchan mode. Go ahead and check for an available lchan. */ - ts_as_pchan_for_each_lchan(lchan, ts, as_pchan) { - if (lchan->fi->state == LCHAN_ST_UNUSED) { - LOGPLCHANALLOC("%s ss=%d is available%s\n", + lchans_as_pchan = pchan_subslots(as_pchan); + ts_for_n_lchans(lchan, ts, lchans_as_pchan) { + struct gsm_lchan *was = found_lchan; + + if (lchan->fi->state != LCHAN_ST_UNUSED) { + LOGPLCHANALLOC("%s ss=%d in type=%s,state=%s not suitable\n", gsm_ts_and_pchan_name(ts), lchan->nr, - ts->pchan_is != as_pchan ? " after dyn PCHAN change" : ""); - return lchan; + gsm_chan_t_name(lchan->type), + osmo_fsm_inst_state_name(lchan->fi)); + continue; } - LOGPLCHANALLOC("%s ss=%d in type=%s,state=%s not suitable\n", - gsm_ts_and_pchan_name(ts), lchan->nr, - gsm_lchant_name(lchan->type), - osmo_fsm_inst_state_name(lchan->fi)); + + found_lchan = pick_better_lchan(found_lchan, lchan); + if (found_lchan != was) + LOGPLCHANALLOC("%s ss=%d interf=%u=%ddBm is %s%s\n", + gsm_ts_and_pchan_name(ts), lchan->nr, + lchan->interf_band, lchan->interf_dbm, + was == NULL ? "available" : "better", + ts->pchan_is != as_pchan ? ", after dyn PCHAN change" : ""); + else + LOGPLCHANALLOC("%s ss=%d interf=%u=%ddBm is also available but not better\n", + gsm_ts_and_pchan_name(ts), lchan->nr, + lchan->interf_band, lchan->interf_dbm); + + /* When picking an lchan with least interference, continue to loop across all lchans. When + * ignoring interference levels, return the first match. */ + if (found_lchan && !ts->trx->bts->chan_alloc_avoid_interf) + return found_lchan; } } - return NULL; + if (found_lchan) + LOGPLCHANALLOC("%s ss=%d interf=%ddBm%s is the best pick\n", + gsm_ts_and_pchan_name(found_lchan->ts), found_lchan->nr, + found_lchan->interf_dbm, + found_lchan->ts->pchan_is != as_pchan ? ", after dyn PCHAN change," : ""); + else + LOGPLCHANALLOC("Nothing found\n"); + return found_lchan; #undef LOGPLCHANALLOC } -static struct gsm_lchan * -_lc_dyn_find_bts(struct gsm_bts *bts, enum gsm_phys_chan_config pchan, - enum gsm_phys_chan_config dyn_as_pchan) +static struct gsm_lchan *lc_dyn_find(struct lchan_select_ts_list *ts_list, + enum gsm_phys_chan_config pchan, + enum gsm_phys_chan_config dyn_as_pchan, + bool log) { - struct gsm_bts_trx *trx; - struct gsm_lchan *lc; + struct gsm_lchan *lchan; - if (bts->chan_alloc_reverse) { - llist_for_each_entry_reverse(trx, &bts->trx_list, list) { - lc = _lc_find_trx(trx, pchan, dyn_as_pchan); - if (lc) - return lc; - } - } else { - llist_for_each_entry(trx, &bts->trx_list, list) { - lc = _lc_find_trx(trx, pchan, dyn_as_pchan); - if (lc) - return lc; - } - } + /* First find an lchan that needs no change in its timeslot pchan mode. + * In particular, this ensures that handover to a dynamic timeslot in TCH/H favors timeslots that are currently + * using only one of two TCH/H, so that we don't switch more dynamic timeslots to TCH/H than necessary. + * For non-dynamic timeslots, it is not necessary to do a second pass with allow_pchan_switch == + * true, because they never switch anyway. */ + if ((lchan = _lc_find(ts_list, pchan, dyn_as_pchan, false, log))) + return lchan; + if ((lchan = _lc_find(ts_list, pchan, dyn_as_pchan, true, log))) + return lchan; return NULL; } -static struct gsm_lchan * -_lc_find_bts(struct gsm_bts *bts, enum gsm_phys_chan_config pchan) +static struct gsm_lchan *lc_find(struct lchan_select_ts_list *ts_list, + enum gsm_phys_chan_config pchan, + bool log) { - return _lc_dyn_find_bts(bts, pchan, pchan); + return _lc_find(ts_list, pchan, pchan, false, log); } -struct gsm_lchan *lchan_select_by_chan_mode(struct gsm_bts *bts, - enum gsm48_chan_mode chan_mode, enum channel_rate chan_rate) +enum gsm_chan_t chan_mode_to_chan_type(enum gsm48_chan_mode chan_mode, enum channel_rate chan_rate) { - enum gsm_chan_t type; - - switch (chan_mode) { + switch (gsm48_chan_mode_to_non_vamos(chan_mode)) { case GSM48_CMODE_SIGN: switch (chan_rate) { - case CH_RATE_SDCCH: type = GSM_LCHAN_SDCCH; break; - case CH_RATE_HALF: type = GSM_LCHAN_TCH_H; break; - case CH_RATE_FULL: type = GSM_LCHAN_TCH_F; break; - default: return NULL; + case CH_RATE_SDCCH: + return GSM_LCHAN_SDCCH; + case CH_RATE_HALF: + return GSM_LCHAN_TCH_H; + case CH_RATE_FULL: + return GSM_LCHAN_TCH_F; + default: + return GSM_LCHAN_NONE; } - break; case GSM48_CMODE_SPEECH_EFR: /* EFR works over FR channels only */ if (chan_rate != CH_RATE_FULL) - return NULL; + return GSM_LCHAN_NONE; /* fall through */ case GSM48_CMODE_SPEECH_V1: case GSM48_CMODE_SPEECH_AMR: switch (chan_rate) { - case CH_RATE_HALF: type = GSM_LCHAN_TCH_H; break; - case CH_RATE_FULL: type = GSM_LCHAN_TCH_F; break; - default: return NULL; + case CH_RATE_HALF: + return GSM_LCHAN_TCH_H; + case CH_RATE_FULL: + return GSM_LCHAN_TCH_F; + default: + return GSM_LCHAN_NONE; } - break; default: - return NULL; + return GSM_LCHAN_NONE; } +} + +static int qsort_func(const void *_a, const void *_b) +{ + const struct gsm_bts_trx *trx_a = *(const struct gsm_bts_trx **)_a; + const struct gsm_bts_trx *trx_b = *(const struct gsm_bts_trx **)_b; - return lchan_select_by_type(bts, type); + int pwr_a = trx_a->nominal_power - trx_a->max_power_red; + int pwr_b = trx_b->nominal_power - trx_b->max_power_red; + + /* Sort in descending order */ + return pwr_b - pwr_a; } -struct gsm_lchan *lchan_avail_by_type(struct gsm_bts *bts, enum gsm_chan_t type) +static void populate_ts_list(struct lchan_select_ts_list *ts_list, + struct gsm_bts *bts, + bool chan_alloc_reverse, + bool sort_by_trx_power, + bool log) +{ + struct gsm_bts_trx **trx_list; + struct gsm_bts_trx *trx; + unsigned int num = 0; + + /* Allocate an array with pointers to all TRX instances of a BTS */ + trx_list = talloc_array_ptrtype(bts, trx_list, bts->num_trx); + OSMO_ASSERT(trx_list != NULL); + + llist_for_each_entry(trx, &bts->trx_list, list) + trx_list[trx->nr] = trx; + + /* Sort by TRX power in descending order (if needed) */ + if (sort_by_trx_power) + qsort(&trx_list[0], bts->num_trx, sizeof(trx), &qsort_func); + + for (unsigned int trxn = 0; trxn < bts->num_trx; trxn++) { + trx = trx_list[trxn]; + for (unsigned int tn = 0; tn < ARRAY_SIZE(trx->ts); tn++) { + struct gsm_bts_trx_ts *ts = &trx->ts[tn]; + if (ts_is_usable(ts)) + ts_list->list[num++] = ts; + else if (log) + LOGP(DRLL, LOGL_DEBUG, "%s is not usable\n", gsm_ts_name(ts)); + } + } + + talloc_free(trx_list); + ts_list->num = num; + + /* Reverse the timeslot list if required */ + if (chan_alloc_reverse) { + for (unsigned int tn = 0; tn < num / 2; tn++) { + struct gsm_bts_trx_ts *temp = ts_list->list[tn]; + ts_list->list[tn] = ts_list->list[num - tn - 1]; + ts_list->list[num - tn - 1] = temp; + } + } +} + +static bool chan_alloc_ass_dynamic_reverse(struct gsm_bts *bts, + void *ctx, bool log) +{ + const struct load_counter *ll = &bts->c0->lchan_load; + const struct gsm_lchan *old_lchan = ctx; + unsigned int lchan_load; + int avg_ul_rxlev; + + OSMO_ASSERT(old_lchan != NULL); + OSMO_ASSERT(old_lchan->ts->trx->bts == bts); + +#define LOG_COND(fmt, args...) do { \ + if (log) \ + LOG_LCHAN(old_lchan, LOGL_DEBUG, fmt, ## args); \ + } while (0) + + /* Condition a) Channel load on the C0 (BCCH carrier) */ + lchan_load = ll->total ? ll->used * 100 / ll->total : 0; + if (lchan_load < bts->chan_alloc_dyn_params.c0_chan_load_thresh) { + LOG_COND("C0 Channel Load %u%% < thresh %u%% => using ascending order\n", + lchan_load, bts->chan_alloc_dyn_params.c0_chan_load_thresh); + return false; + } + + /* Condition b) average Uplink RxLev */ + avg_ul_rxlev = get_meas_rep_avg(old_lchan, TDMA_MEAS_FIELD_RXLEV, + TDMA_MEAS_DIR_UL, TDMA_MEAS_SET_AUTO, + bts->chan_alloc_dyn_params.ul_rxlev_avg_num); + if (avg_ul_rxlev < 0) { + LOG_COND("Unknown AVG UL RxLev => using ascending order\n"); + return false; + } + if (avg_ul_rxlev < bts->chan_alloc_dyn_params.ul_rxlev_thresh) { + LOG_COND("AVG UL RxLev %u < thresh %u => using ascending order\n", + avg_ul_rxlev, bts->chan_alloc_dyn_params.ul_rxlev_thresh); + return false; + } + + LOG_COND("C0 Channel Load %u%% >= thresh %u%% and " + "AVG UL RxLev %u >= thresh %u => using descending order\n", + lchan_load, bts->chan_alloc_dyn_params.c0_chan_load_thresh, + avg_ul_rxlev, bts->chan_alloc_dyn_params.ul_rxlev_thresh); + +#undef LOG_COND + + return true; +} + +struct gsm_lchan *lchan_select_by_chan_mode(struct gsm_bts *bts, + enum gsm48_chan_mode chan_mode, + enum channel_rate chan_rate, + enum lchan_select_reason reason, + void *ctx) +{ + enum gsm_chan_t type = chan_mode_to_chan_type(chan_mode, chan_rate); + if (type == GSM_LCHAN_NONE) + return NULL; + return lchan_select_by_type(bts, type, reason, ctx); +} + +struct gsm_lchan *lchan_avail_by_type(struct gsm_bts *bts, + enum gsm_chan_t type, + enum lchan_select_reason reason, + void *ctx, bool log) { struct gsm_lchan *lchan = NULL; enum gsm_phys_chan_config first, first_cbch, second, second_cbch; + struct lchan_select_ts_list ts_list; + bool sort_by_trx_power = false; + bool chan_alloc_reverse = false; - LOG_BTS(bts, DRLL, LOGL_DEBUG, "lchan_avail_by_type(%s)\n", gsm_lchant_name(type)); + if (log) { + LOG_BTS(bts, DRLL, LOGL_DEBUG, "lchan_avail_by_type(type=%s, reason=%s)\n", + gsm_chan_t_name(type), lchan_select_reason_name(reason)); + } + + switch (reason) { + case SELECT_FOR_MS_CHAN_REQ: + chan_alloc_reverse = bts->chan_alloc_chan_req_reverse; + break; + case SELECT_FOR_ASSIGNMENT: + if (bts->chan_alloc_assignment_dynamic) { + chan_alloc_reverse = chan_alloc_ass_dynamic_reverse(bts, ctx, log); + sort_by_trx_power = bts->chan_alloc_dyn_params.sort_by_trx_power; + } else { + chan_alloc_reverse = bts->chan_alloc_assignment_reverse; + } + break; + case SELECT_FOR_HANDOVER: + chan_alloc_reverse = bts->chan_alloc_handover_reverse; + break; + } + + /* Allocate an array with pointers to all timeslots of a BTS */ + ts_list.list = talloc_array_ptrtype(bts, ts_list.list, bts->num_trx * 8); + if (OSMO_UNLIKELY(ts_list.list == NULL)) + return NULL; + + /* Populate this array with the actual pointers */ + populate_ts_list(&ts_list, bts, chan_alloc_reverse, sort_by_trx_power, log); switch (type) { case GSM_LCHAN_SDCCH: - if (bts->chan_alloc_reverse) { + if (chan_alloc_reverse) { first = GSM_PCHAN_SDCCH8_SACCH8C; first_cbch = GSM_PCHAN_SDCCH8_SACCH8C_CBCH; second = GSM_PCHAN_CCCH_SDCCH4; @@ -183,71 +369,79 @@ struct gsm_lchan *lchan_avail_by_type(struct gsm_bts *bts, enum gsm_chan_t type) second_cbch = GSM_PCHAN_SDCCH8_SACCH8C_CBCH; } - lchan = _lc_find_bts(bts, first); + lchan = lc_find(&ts_list, first, log); if (lchan == NULL) - lchan = _lc_find_bts(bts, first_cbch); + lchan = lc_find(&ts_list, first_cbch, log); if (lchan == NULL) - lchan = _lc_find_bts(bts, second); + lchan = lc_find(&ts_list, second, log); if (lchan == NULL) - lchan = _lc_find_bts(bts, second_cbch); + lchan = lc_find(&ts_list, second_cbch, log); + /* No dedicated SDCCH available -- try fully dynamic + * TCH/F_TCH/H_SDCCH8_PDCH if BTS supports it: */ + if (lchan == NULL && osmo_bts_has_feature(&bts->features, BTS_FEAT_DYN_TS_SDCCH8)) + lchan = lc_dyn_find(&ts_list, GSM_PCHAN_OSMO_DYN, + GSM_PCHAN_SDCCH8_SACCH8C, log); break; case GSM_LCHAN_TCH_F: - lchan = _lc_find_bts(bts, GSM_PCHAN_TCH_F); + lchan = lc_find(&ts_list, GSM_PCHAN_TCH_F, log); /* If we don't have TCH/F available, try dynamic TCH/F_PDCH */ - if (!lchan) { - lchan = _lc_dyn_find_bts(bts, GSM_PCHAN_TCH_F_PDCH, - GSM_PCHAN_TCH_F); - /* TCH/F_PDCH used as TCH/F -- here, type is already - * set to GSM_LCHAN_TCH_F, but for clarity's sake... */ - if (lchan) - type = GSM_LCHAN_TCH_F; - } + if (!lchan) + lchan = lc_dyn_find(&ts_list, GSM_PCHAN_TCH_F_PDCH, + GSM_PCHAN_TCH_F, log); /* Try fully dynamic TCH/F_TCH/H_PDCH as TCH/F... */ - if (!lchan && bts->network->dyn_ts_allow_tch_f) { - lchan = _lc_dyn_find_bts(bts, - GSM_PCHAN_TCH_F_TCH_H_PDCH, - GSM_PCHAN_TCH_F); - if (lchan) - type = GSM_LCHAN_TCH_F; - } + if (!lchan && bts->network->dyn_ts_allow_tch_f) + lchan = lc_dyn_find(&ts_list, GSM_PCHAN_OSMO_DYN, + GSM_PCHAN_TCH_F, log); break; case GSM_LCHAN_TCH_H: - lchan = _lc_find_bts(bts, GSM_PCHAN_TCH_H); + lchan = lc_find(&ts_list, GSM_PCHAN_TCH_H, log); /* No dedicated TCH/x available -- try fully dynamic * TCH/F_TCH/H_PDCH */ - if (!lchan) { - lchan = _lc_dyn_find_bts(bts, - GSM_PCHAN_TCH_F_TCH_H_PDCH, - GSM_PCHAN_TCH_H); - if (lchan) - type = GSM_LCHAN_TCH_H; - } + if (!lchan) + lchan = lc_dyn_find(&ts_list, GSM_PCHAN_OSMO_DYN, + GSM_PCHAN_TCH_H, log); break; default: LOG_BTS(bts, DRLL, LOGL_ERROR, "Unknown gsm_chan_t %u\n", type); } + talloc_free(ts_list.list); + return lchan; } /* Return a matching lchan from a specific BTS that is currently available. The next logical step is * lchan_activate() on it, which would possibly cause dynamic timeslot pchan switching, taken care of by * the lchan and timeslot FSMs. */ -struct gsm_lchan *lchan_select_by_type(struct gsm_bts *bts, enum gsm_chan_t type) +struct gsm_lchan *lchan_select_by_type(struct gsm_bts *bts, + enum gsm_chan_t type, + enum lchan_select_reason reason, + void *ctx) { struct gsm_lchan *lchan = NULL; - lchan = lchan_avail_by_type(bts, type); + LOG_BTS(bts, DRLL, LOGL_DEBUG, "lchan_select_by_type(type=%s, reason=%s)\n", + gsm_chan_t_name(type), lchan_select_reason_name(reason)); - LOG_BTS(bts, DRLL, LOGL_DEBUG, "lchan_select_by_type(%s)\n", gsm_lchant_name(type)); + lchan = lchan_avail_by_type(bts, type, reason, ctx, true); - if (lchan) { - lchan->type = type; - LOG_LCHAN(lchan, LOGL_INFO, "Selected\n"); - } else - LOG_BTS(bts, DRLL, LOGL_NOTICE, "Failed to select %s channel\n", - gsm_lchant_name(type)); + if (!lchan) { + LOG_BTS(bts, DRLL, LOGL_NOTICE, "Failed to select %s channel (%s)\n", + gsm_chan_t_name(type), lchan_select_reason_name(reason)); + return NULL; + } + lchan_select_set_type(lchan, type); return lchan; } + +/* Set available lchan to given type. Usually used on lchan obtained with + * lchan_avail_by_type. The next logical step is lchan_activate() on it, which + * would possibly cause dynamic timeslot pchan switching, taken care of by the + * lchan and timeslot FSMs. */ +void lchan_select_set_type(struct gsm_lchan *lchan, enum gsm_chan_t type) +{ + lchan->type = type; + LOG_LCHAN(lchan, LOGL_INFO, "Selected\n"); +} |