2017-07-04 13:55:12 +00:00
|
|
|
/*
|
|
|
|
* OsmocomBB <-> SDR connection bridge
|
|
|
|
* TDMA scheduler: GSM PHY routines
|
|
|
|
*
|
2019-01-15 10:53:02 +00:00
|
|
|
* (C) 2017-2019 by Vadim Yanitskiy <axilirator@gmail.com>
|
2017-07-04 13:55:12 +00:00
|
|
|
*
|
|
|
|
* All Rights Reserved
|
|
|
|
*
|
|
|
|
* This program is free software; you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
* the Free Software Foundation; either version 2 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 General Public License along
|
|
|
|
* with this program; if not, write to the Free Software Foundation, Inc.,
|
|
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include <error.h>
|
|
|
|
#include <errno.h>
|
|
|
|
#include <string.h>
|
|
|
|
#include <talloc.h>
|
2019-01-15 10:53:02 +00:00
|
|
|
#include <stdbool.h>
|
2017-07-04 13:55:12 +00:00
|
|
|
|
2017-12-17 22:47:28 +00:00
|
|
|
#include <osmocom/gsm/a5.h>
|
2019-05-27 15:34:02 +00:00
|
|
|
#include <osmocom/gsm/protocol/gsm_08_58.h>
|
2017-07-04 13:55:12 +00:00
|
|
|
#include <osmocom/core/bits.h>
|
|
|
|
#include <osmocom/core/msgb.h>
|
|
|
|
#include <osmocom/core/logging.h>
|
|
|
|
#include <osmocom/core/linuxlist.h>
|
|
|
|
|
2019-05-29 23:19:10 +00:00
|
|
|
#include "l1ctl_proto.h"
|
2017-07-04 13:55:12 +00:00
|
|
|
#include "scheduler.h"
|
|
|
|
#include "sched_trx.h"
|
|
|
|
#include "trx_if.h"
|
|
|
|
#include "logging.h"
|
|
|
|
|
|
|
|
static void sched_frame_clck_cb(struct trx_sched *sched)
|
|
|
|
{
|
|
|
|
struct trx_instance *trx = (struct trx_instance *) sched->data;
|
2017-07-12 11:48:18 +00:00
|
|
|
const struct trx_frame *frame;
|
2017-07-31 07:27:30 +00:00
|
|
|
struct trx_lchan_state *lchan;
|
2017-07-12 11:48:18 +00:00
|
|
|
trx_lchan_tx_func *handler;
|
|
|
|
enum trx_lchan_type chan;
|
|
|
|
uint8_t offset, bid;
|
|
|
|
struct trx_ts *ts;
|
|
|
|
uint32_t fn;
|
2017-07-28 08:47:41 +00:00
|
|
|
int i;
|
2017-07-04 13:55:12 +00:00
|
|
|
|
2017-07-28 08:47:41 +00:00
|
|
|
/* Iterate over timeslot list */
|
|
|
|
for (i = 0; i < TRX_TS_COUNT; i++) {
|
|
|
|
/* Timeslot is not allocated */
|
|
|
|
ts = trx->ts_list[i];
|
|
|
|
if (ts == NULL)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
/* Timeslot is not configured */
|
|
|
|
if (ts->mf_layout == NULL)
|
|
|
|
continue;
|
2017-07-04 13:55:12 +00:00
|
|
|
|
2017-11-23 13:05:00 +00:00
|
|
|
/**
|
|
|
|
* Advance frame number, giving the transceiver more
|
|
|
|
* time until a burst must be transmitted...
|
|
|
|
*/
|
2018-09-12 10:54:24 +00:00
|
|
|
fn = TDMA_FN_SUM(sched->fn_counter_proc,
|
|
|
|
sched->fn_counter_advance);
|
2017-11-23 13:05:00 +00:00
|
|
|
|
2017-07-12 11:48:18 +00:00
|
|
|
/* Get frame from multiframe */
|
|
|
|
offset = fn % ts->mf_layout->period;
|
|
|
|
frame = ts->mf_layout->frames + offset;
|
|
|
|
|
|
|
|
/* Get required info from frame */
|
|
|
|
bid = frame->ul_bid;
|
|
|
|
chan = frame->ul_chan;
|
|
|
|
handler = trx_lchan_desc[chan].tx_fn;
|
|
|
|
|
|
|
|
/* Omit lchans without handler */
|
|
|
|
if (!handler)
|
|
|
|
continue;
|
|
|
|
|
2017-07-31 07:27:30 +00:00
|
|
|
/* Make sure that lchan was allocated and activated */
|
|
|
|
lchan = sched_trx_find_lchan(ts, chan);
|
|
|
|
if (lchan == NULL)
|
|
|
|
continue;
|
|
|
|
|
2018-03-10 21:20:57 +00:00
|
|
|
/* Omit inactive lchans */
|
|
|
|
if (!lchan->active)
|
|
|
|
continue;
|
|
|
|
|
2017-12-17 20:47:23 +00:00
|
|
|
/**
|
|
|
|
* If we aren't processing any primitive yet,
|
|
|
|
* attempt to obtain a new one from queue
|
|
|
|
*/
|
|
|
|
if (lchan->prim == NULL)
|
2018-09-27 19:47:54 +00:00
|
|
|
lchan->prim = sched_prim_dequeue(&ts->tx_prims, fn, lchan);
|
2017-12-17 20:47:23 +00:00
|
|
|
|
2018-03-10 21:18:06 +00:00
|
|
|
/* TODO: report TX buffers health to the higher layers */
|
|
|
|
|
|
|
|
/* If CBTX (Continuous Burst Transmission) is assumed */
|
|
|
|
if (trx_lchan_desc[chan].flags & TRX_CH_FLAG_CBTX) {
|
|
|
|
/**
|
|
|
|
* Probably, a TX buffer is empty. Nevertheless,
|
|
|
|
* we shall continuously transmit anything on
|
|
|
|
* CBTX channels.
|
|
|
|
*/
|
|
|
|
if (lchan->prim == NULL)
|
|
|
|
sched_prim_dummy(lchan);
|
|
|
|
}
|
|
|
|
|
2017-12-17 20:47:23 +00:00
|
|
|
/* If there is no primitive, do nothing */
|
|
|
|
if (lchan->prim == NULL)
|
|
|
|
continue;
|
2017-07-12 11:48:18 +00:00
|
|
|
|
2019-05-29 23:19:10 +00:00
|
|
|
/* Handover RACH needs to be handled regardless of the
|
|
|
|
* current channel type and the associated handler. */
|
|
|
|
if (PRIM_IS_RACH(lchan->prim) && lchan->prim->chan != TRXC_RACH)
|
|
|
|
handler = trx_lchan_desc[TRXC_RACH].tx_fn;
|
|
|
|
|
2017-07-12 11:48:18 +00:00
|
|
|
/* Poke lchan handler */
|
2017-12-17 20:47:23 +00:00
|
|
|
handler(trx, ts, lchan, fn, bid);
|
2017-07-12 11:48:18 +00:00
|
|
|
}
|
2017-07-04 13:55:12 +00:00
|
|
|
}
|
|
|
|
|
2017-11-23 13:05:00 +00:00
|
|
|
int sched_trx_init(struct trx_instance *trx, uint32_t fn_advance)
|
2017-07-04 13:55:12 +00:00
|
|
|
{
|
|
|
|
struct trx_sched *sched;
|
|
|
|
|
|
|
|
if (!trx)
|
|
|
|
return -EINVAL;
|
|
|
|
|
|
|
|
LOGP(DSCH, LOGL_NOTICE, "Init scheduler\n");
|
|
|
|
|
|
|
|
/* Obtain a scheduler instance from TRX */
|
|
|
|
sched = &trx->sched;
|
|
|
|
|
|
|
|
/* Register frame clock callback */
|
|
|
|
sched->clock_cb = sched_frame_clck_cb;
|
|
|
|
|
|
|
|
/* Set pointers */
|
|
|
|
sched = &trx->sched;
|
|
|
|
sched->data = trx;
|
|
|
|
|
2017-11-23 13:05:00 +00:00
|
|
|
/* Set frame counter advance */
|
|
|
|
sched->fn_counter_advance = fn_advance;
|
|
|
|
|
2017-07-04 13:55:12 +00:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
int sched_trx_shutdown(struct trx_instance *trx)
|
|
|
|
{
|
|
|
|
int i;
|
|
|
|
|
|
|
|
if (!trx)
|
|
|
|
return -EINVAL;
|
|
|
|
|
|
|
|
LOGP(DSCH, LOGL_NOTICE, "Shutdown scheduler\n");
|
|
|
|
|
|
|
|
/* Free all potentially allocated timeslots */
|
|
|
|
for (i = 0; i < TRX_TS_COUNT; i++)
|
|
|
|
sched_trx_del_ts(trx, i);
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2019-01-15 10:53:02 +00:00
|
|
|
int sched_trx_reset(struct trx_instance *trx, bool reset_clock)
|
2017-07-04 13:55:12 +00:00
|
|
|
{
|
|
|
|
int i;
|
|
|
|
|
|
|
|
if (!trx)
|
|
|
|
return -EINVAL;
|
|
|
|
|
2017-07-27 10:53:09 +00:00
|
|
|
LOGP(DSCH, LOGL_NOTICE, "Reset scheduler %s\n",
|
|
|
|
reset_clock ? "and clock counter" : "");
|
2017-07-04 13:55:12 +00:00
|
|
|
|
|
|
|
/* Free all potentially allocated timeslots */
|
|
|
|
for (i = 0; i < TRX_TS_COUNT; i++)
|
|
|
|
sched_trx_del_ts(trx, i);
|
|
|
|
|
2017-07-27 10:53:09 +00:00
|
|
|
/* Stop and reset clock counter if required */
|
|
|
|
if (reset_clock)
|
|
|
|
sched_clck_reset(&trx->sched);
|
2017-07-27 02:57:13 +00:00
|
|
|
|
2017-07-04 13:55:12 +00:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2017-07-28 08:47:41 +00:00
|
|
|
struct trx_ts *sched_trx_add_ts(struct trx_instance *trx, int tn)
|
2017-07-04 13:55:12 +00:00
|
|
|
{
|
2017-07-28 08:47:41 +00:00
|
|
|
/* Make sure that ts isn't allocated yet */
|
|
|
|
if (trx->ts_list[tn] != NULL) {
|
|
|
|
LOGP(DSCH, LOGL_ERROR, "Timeslot #%u already allocated\n", tn);
|
|
|
|
return NULL;
|
|
|
|
}
|
2017-07-04 13:55:12 +00:00
|
|
|
|
2017-07-28 08:47:41 +00:00
|
|
|
LOGP(DSCH, LOGL_NOTICE, "Add a new TDMA timeslot #%u\n", tn);
|
2017-07-04 13:55:12 +00:00
|
|
|
|
2017-07-28 08:47:41 +00:00
|
|
|
/* Allocate a new one */
|
|
|
|
trx->ts_list[tn] = talloc_zero(trx, struct trx_ts);
|
2017-07-04 13:55:12 +00:00
|
|
|
|
2019-12-04 13:20:05 +00:00
|
|
|
/* Add backpointer */
|
|
|
|
trx->ts_list[tn]->trx = trx;
|
|
|
|
|
2017-07-28 08:47:41 +00:00
|
|
|
/* Assign TS index */
|
|
|
|
trx->ts_list[tn]->index = tn;
|
2017-07-04 13:55:12 +00:00
|
|
|
|
2017-07-28 08:47:41 +00:00
|
|
|
return trx->ts_list[tn];
|
2017-07-04 13:55:12 +00:00
|
|
|
}
|
|
|
|
|
2017-07-28 08:47:41 +00:00
|
|
|
void sched_trx_del_ts(struct trx_instance *trx, int tn)
|
2017-07-04 13:55:12 +00:00
|
|
|
{
|
2018-03-09 08:33:59 +00:00
|
|
|
struct trx_lchan_state *lchan, *lchan_next;
|
2017-07-04 13:55:12 +00:00
|
|
|
struct trx_ts *ts;
|
|
|
|
|
|
|
|
/* Find ts in list */
|
2017-07-28 08:47:41 +00:00
|
|
|
ts = trx->ts_list[tn];
|
2017-07-04 13:55:12 +00:00
|
|
|
if (ts == NULL)
|
|
|
|
return;
|
|
|
|
|
2017-07-28 08:47:41 +00:00
|
|
|
LOGP(DSCH, LOGL_NOTICE, "Delete TDMA timeslot #%u\n", tn);
|
2017-07-04 13:55:12 +00:00
|
|
|
|
2018-01-04 02:17:51 +00:00
|
|
|
/* Deactivate all logical channels */
|
|
|
|
sched_trx_deactivate_all_lchans(ts);
|
|
|
|
|
|
|
|
/* Free channel states */
|
2018-03-09 08:33:59 +00:00
|
|
|
llist_for_each_entry_safe(lchan, lchan_next, &ts->lchans, list) {
|
|
|
|
llist_del(&lchan->list);
|
2018-01-05 00:24:04 +00:00
|
|
|
talloc_free(lchan);
|
2018-03-09 08:33:59 +00:00
|
|
|
}
|
2018-01-04 02:17:51 +00:00
|
|
|
|
2017-07-04 13:55:12 +00:00
|
|
|
/* Flush queue primitives for TX */
|
2017-12-17 19:13:41 +00:00
|
|
|
sched_prim_flush_queue(&ts->tx_prims);
|
2017-07-04 13:55:12 +00:00
|
|
|
|
2017-07-28 08:47:41 +00:00
|
|
|
/* Remove ts from list and free memory */
|
|
|
|
trx->ts_list[tn] = NULL;
|
2017-07-04 13:55:12 +00:00
|
|
|
talloc_free(ts);
|
2017-07-14 02:18:03 +00:00
|
|
|
|
|
|
|
/* Notify transceiver about that */
|
2017-07-28 08:47:41 +00:00
|
|
|
trx_if_cmd_setslot(trx, tn, 0);
|
2017-07-04 13:55:12 +00:00
|
|
|
}
|
|
|
|
|
2018-01-05 00:24:04 +00:00
|
|
|
#define LAYOUT_HAS_LCHAN(layout, lchan) \
|
|
|
|
(layout->lchan_mask & ((uint64_t) 0x01 << lchan))
|
|
|
|
|
2017-07-28 08:47:41 +00:00
|
|
|
int sched_trx_configure_ts(struct trx_instance *trx, int tn,
|
2017-07-04 13:55:12 +00:00
|
|
|
enum gsm_phys_chan_config config)
|
|
|
|
{
|
2018-01-05 00:24:04 +00:00
|
|
|
struct trx_lchan_state *lchan;
|
|
|
|
enum trx_lchan_type type;
|
2017-07-04 13:55:12 +00:00
|
|
|
struct trx_ts *ts;
|
|
|
|
|
|
|
|
/* Try to find specified ts */
|
2017-07-28 08:47:41 +00:00
|
|
|
ts = trx->ts_list[tn];
|
2017-07-04 13:55:12 +00:00
|
|
|
if (ts != NULL) {
|
|
|
|
/* Reconfiguration of existing one */
|
2017-07-28 08:47:41 +00:00
|
|
|
sched_trx_reset_ts(trx, tn);
|
2017-07-04 13:55:12 +00:00
|
|
|
} else {
|
|
|
|
/* Allocate a new one if doesn't exist */
|
2017-07-28 08:47:41 +00:00
|
|
|
ts = sched_trx_add_ts(trx, tn);
|
2017-07-04 13:55:12 +00:00
|
|
|
if (ts == NULL)
|
|
|
|
return -ENOMEM;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Choose proper multiframe layout */
|
2017-07-28 08:47:41 +00:00
|
|
|
ts->mf_layout = sched_mframe_layout(config, tn);
|
2019-12-01 11:59:04 +00:00
|
|
|
if (!ts->mf_layout)
|
|
|
|
return -EINVAL;
|
2017-07-04 13:55:12 +00:00
|
|
|
if (ts->mf_layout->chan_config != config)
|
|
|
|
return -EINVAL;
|
|
|
|
|
2017-07-26 14:28:01 +00:00
|
|
|
LOGP(DSCH, LOGL_NOTICE, "(Re)configure TDMA timeslot #%u as %s\n",
|
2017-07-28 08:47:41 +00:00
|
|
|
tn, ts->mf_layout->name);
|
2017-07-04 13:55:12 +00:00
|
|
|
|
2018-01-05 00:24:04 +00:00
|
|
|
/* Init queue primitives for TX */
|
|
|
|
INIT_LLIST_HEAD(&ts->tx_prims);
|
|
|
|
/* Init logical channels list */
|
|
|
|
INIT_LLIST_HEAD(&ts->lchans);
|
2017-07-04 13:55:12 +00:00
|
|
|
|
|
|
|
/* Allocate channel states */
|
2018-01-05 00:24:04 +00:00
|
|
|
for (type = 0; type < _TRX_CHAN_MAX; type++) {
|
|
|
|
if (!LAYOUT_HAS_LCHAN(ts->mf_layout, type))
|
|
|
|
continue;
|
|
|
|
|
|
|
|
/* Allocate a channel state */
|
|
|
|
lchan = talloc_zero(ts, struct trx_lchan_state);
|
|
|
|
if (!lchan)
|
|
|
|
return -ENOMEM;
|
|
|
|
|
2019-12-04 13:20:05 +00:00
|
|
|
/* set backpointer */
|
|
|
|
lchan->ts = ts;
|
|
|
|
|
2018-01-05 00:24:04 +00:00
|
|
|
/* Set channel type */
|
|
|
|
lchan->type = type;
|
|
|
|
|
|
|
|
/* Add to the list of channel states */
|
|
|
|
llist_add_tail(&lchan->list, &ts->lchans);
|
|
|
|
|
|
|
|
/* Enable channel automatically if required */
|
|
|
|
if (trx_lchan_desc[type].flags & TRX_CH_FLAG_AUTO)
|
|
|
|
sched_trx_activate_lchan(ts, type);
|
2017-07-04 13:55:12 +00:00
|
|
|
}
|
|
|
|
|
2017-07-14 02:18:03 +00:00
|
|
|
/* Notify transceiver about TS activation */
|
|
|
|
/* FIXME: set proper channel type */
|
2017-07-28 08:47:41 +00:00
|
|
|
trx_if_cmd_setslot(trx, tn, 1);
|
2017-07-14 02:18:03 +00:00
|
|
|
|
2017-07-04 13:55:12 +00:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2017-07-28 08:47:41 +00:00
|
|
|
int sched_trx_reset_ts(struct trx_instance *trx, int tn)
|
2017-07-04 13:55:12 +00:00
|
|
|
{
|
2018-01-05 00:24:04 +00:00
|
|
|
struct trx_lchan_state *lchan, *lchan_next;
|
2017-07-04 13:55:12 +00:00
|
|
|
struct trx_ts *ts;
|
|
|
|
|
|
|
|
/* Try to find specified ts */
|
2017-07-28 08:47:41 +00:00
|
|
|
ts = trx->ts_list[tn];
|
2017-07-04 13:55:12 +00:00
|
|
|
if (ts == NULL)
|
|
|
|
return -EINVAL;
|
|
|
|
|
2017-07-08 14:03:22 +00:00
|
|
|
/* Flush TS frame counter */
|
2017-07-04 13:55:12 +00:00
|
|
|
ts->mf_last_fn = 0;
|
|
|
|
|
|
|
|
/* Undefine multiframe layout */
|
|
|
|
ts->mf_layout = NULL;
|
|
|
|
|
|
|
|
/* Flush queue primitives for TX */
|
2017-12-17 19:13:41 +00:00
|
|
|
sched_prim_flush_queue(&ts->tx_prims);
|
2017-07-04 13:55:12 +00:00
|
|
|
|
2018-01-04 02:17:51 +00:00
|
|
|
/* Deactivate all logical channels */
|
|
|
|
sched_trx_deactivate_all_lchans(ts);
|
|
|
|
|
2017-07-04 13:55:12 +00:00
|
|
|
/* Free channel states */
|
2018-01-05 00:24:04 +00:00
|
|
|
llist_for_each_entry_safe(lchan, lchan_next, &ts->lchans, list) {
|
|
|
|
llist_del(&lchan->list);
|
|
|
|
talloc_free(lchan);
|
|
|
|
}
|
2017-07-04 13:55:12 +00:00
|
|
|
|
2017-07-14 02:18:03 +00:00
|
|
|
/* Notify transceiver about that */
|
2017-07-28 08:47:41 +00:00
|
|
|
trx_if_cmd_setslot(trx, tn, 0);
|
2017-07-14 02:18:03 +00:00
|
|
|
|
2017-07-04 13:55:12 +00:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2017-12-17 22:47:28 +00:00
|
|
|
int sched_trx_start_ciphering(struct trx_ts *ts, uint8_t algo,
|
|
|
|
uint8_t *key, uint8_t key_len)
|
|
|
|
{
|
2018-01-05 00:24:04 +00:00
|
|
|
struct trx_lchan_state *lchan;
|
2017-12-17 22:47:28 +00:00
|
|
|
|
|
|
|
/* Prevent NULL-pointer deference */
|
|
|
|
if (!ts)
|
|
|
|
return -EINVAL;
|
|
|
|
|
|
|
|
/* Make sure we can store this key */
|
|
|
|
if (key_len > MAX_A5_KEY_LEN)
|
|
|
|
return -ERANGE;
|
|
|
|
|
|
|
|
/* Iterate over all allocated logical channels */
|
2018-01-05 00:24:04 +00:00
|
|
|
llist_for_each_entry(lchan, &ts->lchans, list) {
|
|
|
|
/* Omit inactive channels */
|
|
|
|
if (!lchan->active)
|
|
|
|
continue;
|
|
|
|
|
2017-12-17 22:47:28 +00:00
|
|
|
/* Set key length and algorithm */
|
2018-01-05 00:24:04 +00:00
|
|
|
lchan->a5.key_len = key_len;
|
|
|
|
lchan->a5.algo = algo;
|
2017-12-17 22:47:28 +00:00
|
|
|
|
|
|
|
/* Copy requested key */
|
|
|
|
if (key_len)
|
2018-01-05 00:24:04 +00:00
|
|
|
memcpy(lchan->a5.key, key, key_len);
|
2017-12-17 22:47:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2017-07-04 13:55:12 +00:00
|
|
|
struct trx_lchan_state *sched_trx_find_lchan(struct trx_ts *ts,
|
|
|
|
enum trx_lchan_type chan)
|
|
|
|
{
|
2018-01-05 00:24:04 +00:00
|
|
|
struct trx_lchan_state *lchan;
|
2017-07-04 13:55:12 +00:00
|
|
|
|
2018-01-05 00:24:04 +00:00
|
|
|
llist_for_each_entry(lchan, &ts->lchans, list)
|
|
|
|
if (lchan->type == chan)
|
|
|
|
return lchan;
|
2017-07-04 13:55:12 +00:00
|
|
|
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
2018-04-02 17:57:55 +00:00
|
|
|
int sched_trx_set_lchans(struct trx_ts *ts, uint8_t chan_nr, int active, uint8_t tch_mode)
|
2017-07-29 17:43:52 +00:00
|
|
|
{
|
|
|
|
const struct trx_lchan_desc *lchan_desc;
|
|
|
|
struct trx_lchan_state *lchan;
|
2018-01-05 00:24:04 +00:00
|
|
|
int rc = 0;
|
2017-07-29 17:43:52 +00:00
|
|
|
|
|
|
|
/* Prevent NULL-pointer deference */
|
2018-01-05 00:24:04 +00:00
|
|
|
if (ts == NULL) {
|
2017-07-29 17:43:52 +00:00
|
|
|
LOGP(DSCH, LOGL_ERROR, "Timeslot isn't configured\n");
|
|
|
|
return -EINVAL;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Iterate over all allocated lchans */
|
2018-01-05 00:24:04 +00:00
|
|
|
llist_for_each_entry(lchan, &ts->lchans, list) {
|
2017-07-29 17:43:52 +00:00
|
|
|
lchan_desc = &trx_lchan_desc[lchan->type];
|
|
|
|
|
|
|
|
if (lchan_desc->chan_nr == (chan_nr & 0xf8)) {
|
2018-04-02 17:57:55 +00:00
|
|
|
if (active) {
|
2017-07-29 17:43:52 +00:00
|
|
|
rc |= sched_trx_activate_lchan(ts, lchan->type);
|
2018-04-02 17:57:55 +00:00
|
|
|
lchan->tch_mode = tch_mode;
|
|
|
|
} else
|
2017-07-29 17:43:52 +00:00
|
|
|
rc |= sched_trx_deactivate_lchan(ts, lchan->type);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return rc;
|
|
|
|
}
|
|
|
|
|
2017-07-04 13:55:12 +00:00
|
|
|
int sched_trx_activate_lchan(struct trx_ts *ts, enum trx_lchan_type chan)
|
|
|
|
{
|
|
|
|
const struct trx_lchan_desc *lchan_desc = &trx_lchan_desc[chan];
|
|
|
|
struct trx_lchan_state *lchan;
|
|
|
|
|
|
|
|
/* Try to find requested logical channel */
|
|
|
|
lchan = sched_trx_find_lchan(ts, chan);
|
|
|
|
if (lchan == NULL)
|
|
|
|
return -EINVAL;
|
|
|
|
|
|
|
|
if (lchan->active) {
|
|
|
|
LOGP(DSCH, LOGL_ERROR, "Logical channel %s already activated "
|
|
|
|
"on ts=%d\n", trx_lchan_desc[chan].name, ts->index);
|
|
|
|
return -EINVAL;
|
|
|
|
}
|
|
|
|
|
2017-07-29 17:43:52 +00:00
|
|
|
LOGP(DSCH, LOGL_NOTICE, "Activating lchan=%s "
|
|
|
|
"on ts=%d\n", trx_lchan_desc[chan].name, ts->index);
|
|
|
|
|
2017-07-04 13:55:12 +00:00
|
|
|
/* Conditionally allocate memory for bursts */
|
|
|
|
if (lchan_desc->rx_fn && lchan_desc->burst_buf_size > 0) {
|
2018-01-05 00:24:04 +00:00
|
|
|
lchan->rx_bursts = talloc_zero_size(lchan,
|
2017-07-04 13:55:12 +00:00
|
|
|
lchan_desc->burst_buf_size);
|
|
|
|
if (lchan->rx_bursts == NULL)
|
|
|
|
return -ENOMEM;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (lchan_desc->tx_fn && lchan_desc->burst_buf_size > 0) {
|
2018-01-05 00:24:04 +00:00
|
|
|
lchan->tx_bursts = talloc_zero_size(lchan,
|
2017-07-04 13:55:12 +00:00
|
|
|
lchan_desc->burst_buf_size);
|
|
|
|
if (lchan->tx_bursts == NULL)
|
|
|
|
return -ENOMEM;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Finally, update channel status */
|
|
|
|
lchan->active = 1;
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2018-01-04 00:26:34 +00:00
|
|
|
static void sched_trx_reset_lchan(struct trx_lchan_state *lchan)
|
|
|
|
{
|
|
|
|
/* Prevent NULL-pointer deference */
|
|
|
|
OSMO_ASSERT(lchan != NULL);
|
|
|
|
|
|
|
|
/* Reset internal state variables */
|
|
|
|
lchan->rx_burst_mask = 0x00;
|
|
|
|
lchan->tx_burst_mask = 0x00;
|
|
|
|
lchan->rx_first_fn = 0;
|
|
|
|
|
|
|
|
/* Free burst memory */
|
|
|
|
talloc_free(lchan->rx_bursts);
|
|
|
|
talloc_free(lchan->tx_bursts);
|
|
|
|
|
|
|
|
lchan->rx_bursts = NULL;
|
|
|
|
lchan->tx_bursts = NULL;
|
|
|
|
|
|
|
|
/* Forget the current prim */
|
|
|
|
sched_prim_drop(lchan);
|
|
|
|
|
trxcon/scheduler: fix Measurement Reporting on SACCH
According to 3GPP TS 04.08, section 3.4.1, SACCH logical channel
accompanies either a traffic or a signaling channel. It has the
particularity that continuous transmission must occur in both
directions, so on the Uplink direction measurement result messages
are sent at each possible occasion when nothing else has to be sent.
The LAPDm fill frames (0x01, 0x03, 0x01, 0x2b, ...) are not
applicable on SACCH channels!
Unfortunately, 3GPP TS 04.08 doesn't clearly state which "else
messages" besides Measurement Reports can be send by the MS on
SACCH channels. However, in sub-clause 3.4.1 it's stated that
the interval between two successive measurement result messages
shall not exceed one L2 frame.
This change introduces a separate handler for SACCH primitives,
which dequeues a SACCH primitive from transmit queue, if present.
Otherwise it dequeues a cached Measurement Report (the last
received one). Finally, if the cache is empty, a "dummy"
measurement report is used. When it's possible,
a non-MR primitive is prioritized.
Change-Id: If1b8dc74ced746d6270676fdde75fcda32f91a3d
Related: OS#2988
2018-03-22 18:40:03 +00:00
|
|
|
/* Channel specific stuff */
|
2018-01-04 00:26:34 +00:00
|
|
|
if (CHAN_IS_TCH(lchan->type)) {
|
|
|
|
lchan->dl_ongoing_facch = 0;
|
2018-08-12 21:48:14 +00:00
|
|
|
lchan->ul_facch_blocks = 0;
|
2018-01-04 00:26:34 +00:00
|
|
|
|
2018-08-15 02:00:16 +00:00
|
|
|
lchan->tch_mode = GSM48_CMODE_SIGN;
|
2018-01-04 00:26:34 +00:00
|
|
|
|
|
|
|
/* Reset AMR state */
|
|
|
|
memset(&lchan->amr, 0x00, sizeof(lchan->amr));
|
trxcon/scheduler: fix Measurement Reporting on SACCH
According to 3GPP TS 04.08, section 3.4.1, SACCH logical channel
accompanies either a traffic or a signaling channel. It has the
particularity that continuous transmission must occur in both
directions, so on the Uplink direction measurement result messages
are sent at each possible occasion when nothing else has to be sent.
The LAPDm fill frames (0x01, 0x03, 0x01, 0x2b, ...) are not
applicable on SACCH channels!
Unfortunately, 3GPP TS 04.08 doesn't clearly state which "else
messages" besides Measurement Reports can be send by the MS on
SACCH channels. However, in sub-clause 3.4.1 it's stated that
the interval between two successive measurement result messages
shall not exceed one L2 frame.
This change introduces a separate handler for SACCH primitives,
which dequeues a SACCH primitive from transmit queue, if present.
Otherwise it dequeues a cached Measurement Report (the last
received one). Finally, if the cache is empty, a "dummy"
measurement report is used. When it's possible,
a non-MR primitive is prioritized.
Change-Id: If1b8dc74ced746d6270676fdde75fcda32f91a3d
Related: OS#2988
2018-03-22 18:40:03 +00:00
|
|
|
} else if (CHAN_IS_SACCH(lchan->type)) {
|
|
|
|
/* Reset SACCH state */
|
|
|
|
memset(&lchan->sacch, 0x00, sizeof(lchan->sacch));
|
2018-01-04 00:26:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/* Reset ciphering state */
|
|
|
|
memset(&lchan->a5, 0x00, sizeof(lchan->a5));
|
|
|
|
}
|
|
|
|
|
2017-07-04 13:55:12 +00:00
|
|
|
int sched_trx_deactivate_lchan(struct trx_ts *ts, enum trx_lchan_type chan)
|
|
|
|
{
|
|
|
|
struct trx_lchan_state *lchan;
|
|
|
|
|
|
|
|
/* Try to find requested logical channel */
|
|
|
|
lchan = sched_trx_find_lchan(ts, chan);
|
|
|
|
if (lchan == NULL)
|
|
|
|
return -EINVAL;
|
|
|
|
|
2017-07-08 12:39:14 +00:00
|
|
|
if (!lchan->active) {
|
2017-07-04 13:55:12 +00:00
|
|
|
LOGP(DSCH, LOGL_ERROR, "Logical channel %s already deactivated "
|
2017-07-08 12:39:14 +00:00
|
|
|
"on ts=%d\n", trx_lchan_desc[chan].name, ts->index);
|
2017-07-04 13:55:12 +00:00
|
|
|
return -EINVAL;
|
|
|
|
}
|
|
|
|
|
2017-07-29 17:43:52 +00:00
|
|
|
LOGP(DSCH, LOGL_DEBUG, "Deactivating lchan=%s "
|
|
|
|
"on ts=%d\n", trx_lchan_desc[chan].name, ts->index);
|
|
|
|
|
2018-01-04 00:26:34 +00:00
|
|
|
/* Reset internal state, free memory */
|
|
|
|
sched_trx_reset_lchan(lchan);
|
2017-12-17 20:47:23 +00:00
|
|
|
|
2018-01-04 00:26:34 +00:00
|
|
|
/* Update activation flag */
|
2017-07-04 13:55:12 +00:00
|
|
|
lchan->active = 0;
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
2017-07-04 14:12:25 +00:00
|
|
|
|
2017-07-15 08:20:35 +00:00
|
|
|
void sched_trx_deactivate_all_lchans(struct trx_ts *ts)
|
|
|
|
{
|
|
|
|
struct trx_lchan_state *lchan;
|
|
|
|
|
2017-07-29 17:43:52 +00:00
|
|
|
LOGP(DSCH, LOGL_DEBUG, "Deactivating all logical channels "
|
|
|
|
"on ts=%d\n", ts->index);
|
|
|
|
|
2018-01-05 00:24:04 +00:00
|
|
|
llist_for_each_entry(lchan, &ts->lchans, list) {
|
2018-01-04 00:26:34 +00:00
|
|
|
/* Omit inactive channels */
|
|
|
|
if (!lchan->active)
|
|
|
|
continue;
|
2017-07-15 08:20:35 +00:00
|
|
|
|
2018-01-04 00:26:34 +00:00
|
|
|
/* Reset internal state, free memory */
|
|
|
|
sched_trx_reset_lchan(lchan);
|
2017-12-17 20:47:23 +00:00
|
|
|
|
2018-01-04 00:26:34 +00:00
|
|
|
/* Update activation flag */
|
2017-07-15 08:20:35 +00:00
|
|
|
lchan->active = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
enum gsm_phys_chan_config sched_trx_chan_nr2pchan_config(uint8_t chan_nr)
|
|
|
|
{
|
|
|
|
uint8_t cbits = chan_nr >> 3;
|
|
|
|
|
2019-05-27 15:34:02 +00:00
|
|
|
if (cbits == ABIS_RSL_CHAN_NR_CBITS_Bm_ACCHs)
|
2017-07-15 08:20:35 +00:00
|
|
|
return GSM_PCHAN_TCH_F;
|
2019-05-27 15:34:02 +00:00
|
|
|
else if ((cbits & 0x1e) == ABIS_RSL_CHAN_NR_CBITS_Lm_ACCHs(0))
|
2017-07-15 08:20:35 +00:00
|
|
|
return GSM_PCHAN_TCH_H;
|
2019-05-27 15:34:02 +00:00
|
|
|
else if ((cbits & 0x1c) == ABIS_RSL_CHAN_NR_CBITS_SDCCH4_ACCH(0))
|
2017-07-15 08:20:35 +00:00
|
|
|
return GSM_PCHAN_CCCH_SDCCH4;
|
2019-05-27 15:34:02 +00:00
|
|
|
else if ((cbits & 0x18) == ABIS_RSL_CHAN_NR_CBITS_SDCCH8_ACCH(0))
|
2017-07-15 08:20:35 +00:00
|
|
|
return GSM_PCHAN_SDCCH8_SACCH8C;
|
2019-05-27 15:34:02 +00:00
|
|
|
else if ((cbits & 0x1f) == ABIS_RSL_CHAN_NR_CBITS_OSMO_CBCH4)
|
2019-05-27 14:53:00 +00:00
|
|
|
return GSM_PCHAN_CCCH_SDCCH4_CBCH;
|
2019-05-27 15:34:02 +00:00
|
|
|
else if ((cbits & 0x1f) == ABIS_RSL_CHAN_NR_CBITS_OSMO_CBCH8)
|
2018-10-01 21:44:06 +00:00
|
|
|
return GSM_PCHAN_SDCCH8_SACCH8C_CBCH;
|
2019-05-28 00:10:48 +00:00
|
|
|
else if ((cbits & 0x1f) == ABIS_RSL_CHAN_NR_CBITS_OSMO_PDCH)
|
|
|
|
return GSM_PCHAN_PDCH;
|
2017-07-15 08:20:35 +00:00
|
|
|
|
|
|
|
return GSM_PCHAN_NONE;
|
|
|
|
}
|
|
|
|
|
2017-07-29 17:43:52 +00:00
|
|
|
enum trx_lchan_type sched_trx_chan_nr2lchan_type(uint8_t chan_nr,
|
|
|
|
uint8_t link_id)
|
2017-07-15 08:20:35 +00:00
|
|
|
{
|
2017-07-29 17:43:52 +00:00
|
|
|
int i;
|
2017-07-15 08:20:35 +00:00
|
|
|
|
2017-07-29 17:43:52 +00:00
|
|
|
/* Iterate over all known lchan types */
|
|
|
|
for (i = 0; i < _TRX_CHAN_MAX; i++)
|
|
|
|
if (trx_lchan_desc[i].chan_nr == (chan_nr & 0xf8))
|
|
|
|
if (trx_lchan_desc[i].link_id == link_id)
|
|
|
|
return i;
|
2017-07-15 08:20:35 +00:00
|
|
|
|
|
|
|
return TRXC_IDLE;
|
|
|
|
}
|
|
|
|
|
2017-12-17 22:47:28 +00:00
|
|
|
static void sched_trx_a5_burst_dec(struct trx_lchan_state *lchan,
|
|
|
|
uint32_t fn, sbit_t *burst)
|
|
|
|
{
|
|
|
|
ubit_t ks[114];
|
|
|
|
int i;
|
|
|
|
|
|
|
|
/* Generate keystream for a DL burst */
|
|
|
|
osmo_a5(lchan->a5.algo, lchan->a5.key, fn, ks, NULL);
|
|
|
|
|
|
|
|
/* Apply keystream over ciphertext */
|
|
|
|
for (i = 0; i < 57; i++) {
|
|
|
|
if (ks[i])
|
|
|
|
burst[i + 3] *= -1;
|
|
|
|
if (ks[i + 57])
|
|
|
|
burst[i + 88] *= -1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void sched_trx_a5_burst_enc(struct trx_lchan_state *lchan,
|
|
|
|
uint32_t fn, ubit_t *burst)
|
|
|
|
{
|
|
|
|
ubit_t ks[114];
|
|
|
|
int i;
|
|
|
|
|
|
|
|
/* Generate keystream for an UL burst */
|
|
|
|
osmo_a5(lchan->a5.algo, lchan->a5.key, fn, NULL, ks);
|
|
|
|
|
|
|
|
/* Apply keystream over plaintext */
|
|
|
|
for (i = 0; i < 57; i++) {
|
|
|
|
burst[i + 3] ^= ks[i];
|
|
|
|
burst[i + 88] ^= ks[i + 57];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-07-28 08:47:41 +00:00
|
|
|
int sched_trx_handle_rx_burst(struct trx_instance *trx, uint8_t tn,
|
2018-03-02 14:24:57 +00:00
|
|
|
uint32_t burst_fn, sbit_t *bits, uint16_t nbits,
|
2020-03-01 19:55:01 +00:00
|
|
|
const struct trx_meas_set *meas)
|
2017-07-04 14:12:25 +00:00
|
|
|
{
|
|
|
|
struct trx_lchan_state *lchan;
|
|
|
|
const struct trx_frame *frame;
|
|
|
|
struct trx_ts *ts;
|
|
|
|
|
|
|
|
trx_lchan_rx_func *handler;
|
|
|
|
enum trx_lchan_type chan;
|
|
|
|
uint32_t fn, elapsed;
|
|
|
|
uint8_t offset, bid;
|
|
|
|
|
2017-07-28 09:00:40 +00:00
|
|
|
/* Check whether required timeslot is allocated and configured */
|
2017-07-28 09:15:05 +00:00
|
|
|
ts = trx->ts_list[tn];
|
2017-07-28 09:00:40 +00:00
|
|
|
if (ts == NULL || ts->mf_layout == NULL) {
|
2017-08-19 06:38:24 +00:00
|
|
|
LOGP(DSCHD, LOGL_DEBUG, "TDMA timeslot #%u isn't configured, "
|
2017-07-28 08:47:41 +00:00
|
|
|
"ignoring burst...\n", tn);
|
2017-07-04 14:12:25 +00:00
|
|
|
return -EINVAL;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Calculate how many frames have been elapsed */
|
2018-09-12 10:54:24 +00:00
|
|
|
elapsed = TDMA_FN_SUB(burst_fn, ts->mf_last_fn);
|
2017-07-04 14:12:25 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* If not too many frames have been elapsed,
|
|
|
|
* start counting from last fn + 1
|
|
|
|
*/
|
|
|
|
if (elapsed < 10)
|
2020-03-03 22:40:08 +00:00
|
|
|
fn = TDMA_FN_SUM(ts->mf_last_fn, 1);
|
2017-07-04 14:12:25 +00:00
|
|
|
else
|
|
|
|
fn = burst_fn;
|
|
|
|
|
|
|
|
while (1) {
|
|
|
|
/* Get frame from multiframe */
|
|
|
|
offset = fn % ts->mf_layout->period;
|
|
|
|
frame = ts->mf_layout->frames + offset;
|
|
|
|
|
|
|
|
/* Get required info from frame */
|
|
|
|
bid = frame->dl_bid;
|
|
|
|
chan = frame->dl_chan;
|
|
|
|
handler = trx_lchan_desc[chan].rx_fn;
|
|
|
|
|
|
|
|
/* Omit bursts which have no handler, like IDLE bursts */
|
|
|
|
if (!handler)
|
|
|
|
goto next_frame;
|
|
|
|
|
|
|
|
/* Find required channel state */
|
|
|
|
lchan = sched_trx_find_lchan(ts, chan);
|
2017-07-28 09:53:59 +00:00
|
|
|
if (lchan == NULL)
|
2017-07-04 14:12:25 +00:00
|
|
|
goto next_frame;
|
|
|
|
|
|
|
|
/* Ensure that channel is active */
|
|
|
|
if (!lchan->active)
|
|
|
|
goto next_frame;
|
|
|
|
|
2017-12-17 22:47:28 +00:00
|
|
|
/* Reached current fn */
|
2017-07-04 14:12:25 +00:00
|
|
|
if (fn == burst_fn) {
|
2017-12-17 22:47:28 +00:00
|
|
|
/* Perform A5/X decryption if required */
|
|
|
|
if (lchan->a5.algo)
|
|
|
|
sched_trx_a5_burst_dec(lchan, fn, bits);
|
|
|
|
|
|
|
|
/* Put burst to handler */
|
2020-03-01 19:55:01 +00:00
|
|
|
handler(trx, ts, lchan, fn, bid, bits, meas);
|
2017-07-04 14:12:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
next_frame:
|
|
|
|
/* Reached current fn */
|
|
|
|
if (fn == burst_fn)
|
|
|
|
break;
|
|
|
|
|
2020-03-03 22:40:08 +00:00
|
|
|
TDMA_FN_INC(&fn);
|
2017-07-04 14:12:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/* Set last processed frame number */
|
|
|
|
ts->mf_last_fn = fn;
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
2017-12-17 22:45:27 +00:00
|
|
|
|
|
|
|
int sched_trx_handle_tx_burst(struct trx_instance *trx,
|
|
|
|
struct trx_ts *ts, struct trx_lchan_state *lchan,
|
|
|
|
uint32_t fn, ubit_t *bits)
|
|
|
|
{
|
|
|
|
int rc;
|
|
|
|
|
2017-12-17 22:47:28 +00:00
|
|
|
/* Perform A5/X burst encryption if required */
|
|
|
|
if (lchan->a5.algo)
|
|
|
|
sched_trx_a5_burst_enc(lchan, fn, bits);
|
2017-12-17 22:45:27 +00:00
|
|
|
|
|
|
|
/* Forward burst to transceiver */
|
|
|
|
rc = trx_if_tx_burst(trx, ts->index, fn, trx->tx_power, bits);
|
|
|
|
if (rc) {
|
|
|
|
LOGP(DSCHD, LOGL_ERROR, "Could not send burst to transceiver\n");
|
|
|
|
return rc;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
2020-03-01 19:55:01 +00:00
|
|
|
|
|
|
|
#define MEAS_HIST_FIRST(hist) \
|
|
|
|
(&hist->buf[0])
|
|
|
|
#define MEAS_HIST_LAST(hist) \
|
|
|
|
(MEAS_HIST_FIRST(hist) + ARRAY_SIZE(hist->buf) - 1)
|
|
|
|
|
|
|
|
/* Add a new set of measurements to the history */
|
|
|
|
void sched_trx_meas_push(struct trx_lchan_state *lchan, const struct trx_meas_set *meas)
|
|
|
|
{
|
|
|
|
struct trx_lchan_meas_hist *hist = &lchan->meas_hist;
|
|
|
|
|
|
|
|
/* Find a new position where to store the measurements */
|
|
|
|
if (hist->head == MEAS_HIST_LAST(hist) || hist->head == NULL)
|
|
|
|
hist->head = MEAS_HIST_FIRST(hist);
|
|
|
|
else
|
|
|
|
hist->head++;
|
|
|
|
|
|
|
|
*hist->head = *meas;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Calculate the AVG of n measurements from the history */
|
|
|
|
void sched_trx_meas_avg(struct trx_lchan_state *lchan, unsigned int n)
|
|
|
|
{
|
|
|
|
struct trx_lchan_meas_hist *hist = &lchan->meas_hist;
|
|
|
|
struct trx_meas_set *meas = hist->head;
|
|
|
|
int toa256_sum = 0;
|
|
|
|
int rssi_sum = 0;
|
|
|
|
int i;
|
|
|
|
|
|
|
|
OSMO_ASSERT(n > 0 && n <= ARRAY_SIZE(hist->buf));
|
|
|
|
OSMO_ASSERT(meas != NULL);
|
|
|
|
|
|
|
|
/* Traverse backwards up to n entries, calculate the sum */
|
|
|
|
for (i = 0; i < n; i++) {
|
|
|
|
toa256_sum += meas->toa256;
|
|
|
|
rssi_sum += meas->rssi;
|
|
|
|
|
|
|
|
if (meas == MEAS_HIST_FIRST(hist))
|
|
|
|
meas = MEAS_HIST_LAST(hist);
|
|
|
|
else
|
|
|
|
meas--;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Calculate the AVG */
|
|
|
|
lchan->meas_avg.toa256 = toa256_sum / n;
|
|
|
|
lchan->meas_avg.rssi = rssi_sum / n;
|
|
|
|
}
|