Commit ac94f9c4 authored by francescomani's avatar francescomani

new structure to handle beams

parent 9055fabe
......@@ -51,10 +51,12 @@
*
*********************************************************************/
int set_tdd_config_nr( nfapi_nr_config_request_scf_t *cfg,
int mu,
int nrofDownlinkSlots, int nrofDownlinkSymbols,
int nrofUplinkSlots, int nrofUplinkSymbols)
int set_tdd_config_nr(nfapi_nr_config_request_scf_t *cfg,
int mu,
int nrofDownlinkSlots,
int nrofDownlinkSymbols,
int nrofUplinkSlots,
int nrofUplinkSymbols)
{
int slot_number = 0;
......
......@@ -70,6 +70,9 @@
#define CONFIG_STRING_MACRLC_MIN_GRANT_PRB "min_grant_prb"
#define CONFIG_STRING_MACRLC_MIN_GRANT_MCS "min_grant_mcs"
#define CONFIG_STRING_MACRLC_IDENTITY_PM "identity_precoding_matrix"
#define CONFIG_STRING_MACRLC_ANALOG_BEAMFORMING "set_analog_beamforming"
#define CONFIG_STRING_MACRLC_AB_PERIOD "beamforming_period"
#define CONFIG_STRING_MACRLC_BEAMS_PERIOD "beams_per_period"
#define HLP_MACRLC_UL_PRBBLACK "SNR threshold to decide whether a PRB will be blacklisted or not"
#define HLP_MACRLC_DL_BLER_UP "Upper threshold of BLER to decrease DL MCS"
......@@ -83,6 +86,9 @@
#define HLP_MACRLC_MIN_GRANT_PRB "Minimal Periodic ULSCH Grant PRBs"
#define HLP_MACRLC_MIN_GRANT_MCS "Minimal Periodic ULSCH Grant MCS"
#define HLP_MACRLC_IDENTITY_PM "Flag to use only identity matrix in DL precoding"
#define HLP_MACRLC_AB "Flag to enable analog beamforming"
#define HLP_MACRLC_AB_PERIOD "number of consecutive slots for a given set of beams"
#define HLP_MACRLC_BEAMS_PERIOD "set of beams that can be simultaneously allocated in a period"
/*-------------------------------------------------------------------------------------------------------------------------------------------------------*/
/* MacRLC configuration parameters */
......@@ -123,6 +129,9 @@
{CONFIG_STRING_MACRLC_MIN_GRANT_MCS, HLP_MACRLC_MIN_GRANT_MCS, 0, .u8ptr=NULL, .defintval=9, TYPE_UINT8, 0}, \
{CONFIG_STRING_MACRLC_IDENTITY_PM, HLP_MACRLC_IDENTITY_PM, PARAMFLAG_BOOL, .u8ptr=NULL, .defintval=0, TYPE_UINT8, 0}, \
{CONFIG_STRING_MACRLC_LOCAL_N_ADDRESS_F1U, NULL, 0, .strptr=NULL, .defstrval=NULL, TYPE_STRING, 0}, \
{CONFIG_STRING_MACRLC_ANALOG_BEAMFORMING, HLP_MACRLC_AB, PARAMFLAG_BOOL, .u8ptr=NULL, .defintval=0, TYPE_UINT8, 0}, \
{CONFIG_STRING_MACRLC_AB_PERIOD, HLP_MACRLC_AB_PERIOD, 0, .u8ptr=NULL, .defintval=1, TYPE_UINT8, 0}, \
{CONFIG_STRING_MACRLC_BEAMS_PERIOD, HLP_MACRLC_BEAMS_PERIOD, 0, .u8ptr=NULL, .defintval=1, TYPE_UINT8, 0}, \
}
// clang-format off
......@@ -159,6 +168,9 @@
#define MACRLC_MIN_GRANT_MCS_IDX 30
#define MACRLC_IDENTITY_PM_IDX 31
#define MACRLC_LOCAL_N_ADDRESS_F1U_IDX 32
#define MACRLC_ANALOG_BEAMFORMING_IDX 33
#define MACRLC_ANALOG_BEAMFORMING_PERIOD_IDX 34
#define MACRLC_ANALOG_BEAMS_PERIOD_IDX 35
#define MACRLCPARAMS_CHECK { \
{ .s5 = { NULL } }, \
......@@ -192,8 +204,11 @@
{ .s2 = { config_check_intrange, {1, 8} } }, /* UL max HARQ rounds */ \
{ .s5 = { NULL } }, \
{ .s5 = { NULL } }, \
{ .s5 = { NULL } }, \
{ .s2 = { NULL } }, \
{ .s5 = { NULL } }, \
{ .s5 = { NULL } }, \
{ .s5 = { NULL } }, \
}
/*---------------------------------------------------------------------------------------------------------------------------------------------------------*/
......
......@@ -1371,6 +1371,15 @@ void RCconfig_nr_macrlc(configmodule_interface_t *cfg)
RC.nrmac[j]->identity_pm = *(MacRLC_ParamList.paramarray[j][MACRLC_IDENTITY_PM_IDX].u8ptr);
RC.nrmac[j]->num_ulprbbl = num_prbbl;
memcpy(RC.nrmac[j]->ulprbbl, prbbl, 275 * sizeof(prbbl[0]));
bool ab = *(MacRLC_ParamList.paramarray[j][MACRLC_ANALOG_BEAMFORMING_IDX].u8ptr);
if (ab) {
NR_beam_info_t *beam_info = &RC.nrmac[j]->beam_info;
int beams_per_period = *(MacRLC_ParamList.paramarray[j][MACRLC_ANALOG_BEAMS_PERIOD_IDX].u8ptr);
beam_info->beam_allocation = malloc16(beams_per_period * sizeof(int *));
beam_info->slots_in_beam_period = *(MacRLC_ParamList.paramarray[j][MACRLC_ANALOG_BEAMFORMING_PERIOD_IDX].u8ptr);
beam_info->beams_per_period = beams_per_period;
beam_info->beam_allocation_size = -1; // to be initialized once we have information on frame configuration
}
} // for (j=0;j<RC.nb_nr_macrlc_inst;j++)
uint64_t gnb_du_id = 0;
......
......@@ -1770,7 +1770,7 @@ int get_nr_prach_info_from_index(uint8_t index,
uint8_t *N_dur,
uint16_t *RA_sfn_index,
uint8_t *N_RA_slot,
uint8_t *config_period) {
uint8_t *config_period) {
int x,y;
int64_t s_map;
......
......@@ -584,14 +584,6 @@ static void config_common(gNB_MAC_INST *nrmac, nr_pdsch_AntennaPorts_t pdsch_Ant
scc->tdd_UL_DL_ConfigurationCommon->pattern1.nrofUplinkSymbols);
AssertFatal(periods_per_frame > 0, "TDD configuration cannot be configured\n");
if (frequency_range == FR2) {
LOG_I(NR_MAC, "Configuring TDD beam association to default\n");
nrmac->tdd_beam_association = malloc16(periods_per_frame * sizeof(int16_t));
for (int i = 0; i < periods_per_frame; ++i)
nrmac->tdd_beam_association[i] = -1; /* default: beams not configured */
} else {
nrmac->tdd_beam_association = NULL; /* default: no beams */
}
}
// precoding matrix configuration (to be improved)
......
......@@ -146,6 +146,36 @@ void clear_nr_nfapi_information(gNB_MAC_INST *gNB,
TX_req[CC_idP].Number_of_PDUs = 0;
}
void clear_beam_information(NR_beam_info_t *beam_info, int frame, int slot, int mu)
{
// for now we use the same logic of UL_tti_req_ahead
// reset after 1 frame with the exception of 15kHz
if(!beam_info->beam_allocation)
return;
// initialization done only once
const int slots_per_frame = nr_slots_per_frame[mu];
if(beam_info->beam_allocation_size == -1) {
int size = mu == 0 ? slots_per_frame << 1 : slots_per_frame;
// slots in beam period gives the number of consecutive slots tied the the same beam
AssertFatal(size % beam_info->slots_in_beam_period == 0,
"Beam period %d should be divider of number of slots per frame %d\n",
beam_info->slots_in_beam_period, slots_per_frame);
beam_info->beam_allocation_size = size / beam_info->slots_in_beam_period;
for (int i = 0; i < beam_info->beams_per_period; i++) {
beam_info->beam_allocation[i] = malloc16(beam_info->beam_allocation_size * sizeof(int));
for (int j = 0; j < beam_info->beam_allocation_size; j++)
beam_info->beam_allocation[i][j] = -1;
}
}
int idx_to_clear = (frame * slots_per_frame + slot) / beam_info->slots_in_beam_period ;
idx_to_clear = (idx_to_clear + beam_info->beam_allocation_size - 1) % beam_info->beam_allocation_size;
if (slot % beam_info->slots_in_beam_period == 0) {
// resetting previous period allocation
for (int i = 0; i < beam_info->beams_per_period; i++)
beam_info->beam_allocation[i][idx_to_clear] = -1;
}
}
bool is_xlsch_in_slot(uint64_t bitmap, sub_frame_t slot) {
return (bitmap >> (slot % 64)) & 0x01;
}
......@@ -195,19 +225,11 @@ void gNB_dlsch_ulsch_scheduler(module_id_t module_idP, frame_t frame, sub_frame_
gNB_MAC_INST *gNB = RC.nrmac[module_idP];
NR_COMMON_channels_t *cc = gNB->common_channels;
NR_ServingCellConfigCommon_t *scc = cc->ServingCellConfigCommon;
NR_ServingCellConfigCommon_t *scc = cc->ServingCellConfigCommon;
NR_SCHED_LOCK(&gNB->sched_lock);
if (slot==0 && (*scc->downlinkConfigCommon->frequencyInfoDL->frequencyBandList.list.array[0]>=257)) {
//FR2
const NR_TDD_UL_DL_Pattern_t *tdd = &scc->tdd_UL_DL_ConfigurationCommon->pattern1;
AssertFatal(tdd,"Dynamic TDD not handled yet\n");
const int nb_periods_per_frame = get_nb_periods_per_frame(tdd->dl_UL_TransmissionPeriodicity);
// re-initialization of tdd_beam_association at beginning of frame
for (int i=0; i<nb_periods_per_frame; i++)
gNB->tdd_beam_association[i] = -1;
}
clear_beam_information(&gNB->beam_info, frame, slot, *scc->ssbSubcarrierSpacing);
gNB->frame = frame;
start_meas(&gNB->eNB_scheduler);
......
......@@ -116,10 +116,8 @@ void schedule_nr_mib(module_id_t module_idP, frame_t frameP, sub_frame_t slotP,
gNB_MAC_INST *gNB = RC.nrmac[module_idP];
/* already mutex protected: held in gNB_dlsch_ulsch_scheduler() */
nfapi_nr_dl_tti_request_body_t *dl_req;
uint8_t num_tdd_period,num_ssb;
int CC_id;
for (CC_id = 0; CC_id < MAX_NUM_CCs; CC_id++) {
for (int CC_id = 0; CC_id < MAX_NUM_CCs; CC_id++) {
NR_COMMON_channels_t *cc= &gNB->common_channels[CC_id];
const NR_MIB_t *mib = cc->mib->message.choice.mib;
NR_ServingCellConfigCommon_t *scc = cc->ServingCellConfigCommon;
......@@ -170,15 +168,18 @@ void schedule_nr_mib(module_id_t module_idP, frame_t frameP, sub_frame_t slotP,
const BIT_STRING_t *longBitmap = &scc->ssb_PositionsInBurst->choice.longBitmap;
uint16_t ssb_start_symbol;
const int n_slots_frame = nr_slots_per_frame[scs];
switch (scc->ssb_PositionsInBurst->present) {
case 1:
// short bitmap (<3GHz) max 4 SSBs
for (int i_ssb=0; i_ssb<4; i_ssb++) {
if ((shortBitmap->buf[0]>>(7-i_ssb))&0x01) {
ssb_start_symbol = get_ssb_start_symbol(band,scs,i_ssb);
for (int i_ssb = 0; i_ssb < 4; i_ssb++) {
if ((shortBitmap->buf[0] >> (7 - i_ssb)) & 0x01) {
ssb_start_symbol = get_ssb_start_symbol(band, scs, i_ssb);
// if start symbol is in current slot, schedule current SSB, fill VRB map and call get_type0_PDCCH_CSS_config_parameters
if ((ssb_start_symbol/14) == rel_slot){
if ((ssb_start_symbol / 14) == rel_slot){
bool ret = beam_allocation_procedure(&gNB->beam_info, frameP, slotP, i_ssb, n_slots_frame);
AssertFatal(ret, "Cannot allocate SSB %d in any available beam\n", i_ssb);
const int prb_offset = offset_pointa >> scs;
schedule_ssb(frameP, slotP, scc, dl_req, i_ssb, ssbSubcarrierOffset, offset_pointa, mib_pdu);
fill_ssb_vrb_map(cc, prb_offset, ssbSubcarrierOffset, ssb_start_symbol, CC_id);
......@@ -203,11 +204,13 @@ void schedule_nr_mib(module_id_t module_idP, frame_t frameP, sub_frame_t slotP,
break;
case 2:
// medium bitmap (<6GHz) max 8 SSBs
for (int i_ssb=0; i_ssb<8; i_ssb++) {
if ((mediumBitmap->buf[0]>>(7-i_ssb))&0x01) {
ssb_start_symbol = get_ssb_start_symbol(band,scs,i_ssb);
for (int i_ssb = 0; i_ssb < 8; i_ssb++) {
if ((mediumBitmap->buf[0] >> (7 - i_ssb)) & 0x01) {
ssb_start_symbol = get_ssb_start_symbol(band, scs, i_ssb);
// if start symbol is in current slot, schedule current SSB, fill VRB map and call get_type0_PDCCH_CSS_config_parameters
if ((ssb_start_symbol/14) == rel_slot){
if ((ssb_start_symbol / 14) == rel_slot){
bool ret = beam_allocation_procedure(&gNB->beam_info, frameP, slotP, i_ssb, n_slots_frame);
AssertFatal(ret, "Cannot allocate SSB %d in any available beam\n", i_ssb);
const int prb_offset = offset_pointa >> scs;
schedule_ssb(frameP, slotP, scc, dl_req, i_ssb, ssbSubcarrierOffset, offset_pointa, mib_pdu);
fill_ssb_vrb_map(cc, prb_offset, ssbSubcarrierOffset, ssb_start_symbol, CC_id);
......@@ -232,23 +235,16 @@ void schedule_nr_mib(module_id_t module_idP, frame_t frameP, sub_frame_t slotP,
break;
case 3:
// long bitmap FR2 max 64 SSBs
num_ssb = 0;
for (int i_ssb=0; i_ssb<64; i_ssb++) {
if ((longBitmap->buf[i_ssb/8]>>(7-(i_ssb%8)))&0x01) {
ssb_start_symbol = get_ssb_start_symbol(band,scs,i_ssb);
for (int i_ssb = 0; i_ssb < 64; i_ssb++) {
if ((longBitmap->buf[i_ssb / 8] >> (7 - (i_ssb % 8))) & 0x01) {
ssb_start_symbol = get_ssb_start_symbol(band, scs, i_ssb);
// if start symbol is in current slot, schedule current SSB, fill VRB map and call get_type0_PDCCH_CSS_config_parameters
if ((ssb_start_symbol/14) == rel_slot){
if ((ssb_start_symbol / 14) == rel_slot){
bool ret = beam_allocation_procedure(&gNB->beam_info, frameP, slotP, i_ssb, n_slots_frame);
AssertFatal(ret, "Cannot allocate SSB %d in any available beam\n", i_ssb);
const int prb_offset = offset_pointa >> (scs-2); // reference 60kHz
schedule_ssb(frameP, slotP, scc, dl_req, i_ssb, ssbSubcarrierOffset, offset_pointa, mib_pdu);
fill_ssb_vrb_map(cc, prb_offset, ssbSubcarrierOffset >> (scs - 2), ssb_start_symbol, CC_id);
const NR_TDD_UL_DL_Pattern_t *tdd = &scc->tdd_UL_DL_ConfigurationCommon->pattern1;
const int n_slots_frame = nr_slots_per_frame[*scc->ssbSubcarrierSpacing];
// FR2 is only TDD, to be fixed for flexible TDD
const int nr_slots_period = tdd ? n_slots_frame/get_nb_periods_per_frame(scc->tdd_UL_DL_ConfigurationCommon->pattern1.dl_UL_TransmissionPeriodicity) : n_slots_frame;
num_tdd_period = rel_slot/nr_slots_period;
gNB->tdd_beam_association[num_tdd_period]=i_ssb;
num_ssb++;
AssertFatal(num_ssb<2,"beamforming currently not supported for more than one SSB per slot\n");
if (get_softmodem_params()->sa == 1) {
get_type0_PDCCH_CSS_config_parameters(&gNB->type0_PDCCH_CSS_config[i_ssb],
frameP,
......@@ -572,15 +568,18 @@ void schedule_nr_sib1(module_id_t module_idP,
scc->ssb_PositionsInBurst->present);
}
for (int i=0; i<L_max; i++) {
for (int i = 0; i < L_max; i++) {
NR_Type0_PDCCH_CSS_config_t *type0_PDCCH_CSS_config = &gNB_mac->type0_PDCCH_CSS_config[i];
if((frameP%2 == type0_PDCCH_CSS_config->sfn_c) &&
if((frameP % 2 == type0_PDCCH_CSS_config->sfn_c) &&
(slotP == type0_PDCCH_CSS_config->n_0) &&
(type0_PDCCH_CSS_config->num_rbs > 0) &&
(type0_PDCCH_CSS_config->active == true)) {
const int n_slots_frame = nr_slots_per_frame[*scc->ssbSubcarrierSpacing];
bool ret = beam_allocation_procedure(&gNB_mac->beam_info, frameP, slotP, i, n_slots_frame);
AssertFatal(ret, "Cannot allocate SIB1 corresponding to SSB %d in any available beam\n", i);
LOG_D(NR_MAC,"(%d.%d) SIB1 transmission: ssb_index %d\n", frameP, slotP, type0_PDCCH_CSS_config->ssb_index);
default_table_type_t table_type = get_default_table_type(type0_PDCCH_CSS_config->type0_pdcch_ss_mux_pattern);
......
......@@ -555,7 +555,8 @@ static bool allocate_dl_retransmission(module_id_t module_id,
* allocation after CCE alloc fail would be more complex) */
int r_pucch = nr_get_pucch_resource(sched_ctrl->coreset, ul_bwp->pucch_Config, CCEIndex);
const int alloc = nr_acknack_scheduling(nr_mac, UE, frame, slot, r_pucch, 0);
// TODO properly set the beam index (currently only done for RA)
const int alloc = nr_acknack_scheduling(nr_mac, UE, frame, slot, 0, r_pucch, 0);
if (alloc<0) {
LOG_D(NR_MAC, "[UE %04x][%4d.%2d] could not find PUCCH for DL DCI retransmission\n",
UE->rnti,
......@@ -743,7 +744,8 @@ static void pf_dl(module_id_t module_id,
* allocation after CCE alloc fail would be more complex) */
int r_pucch = nr_get_pucch_resource(sched_ctrl->coreset, ul_bwp->pucch_Config, CCEIndex);
const int alloc = nr_acknack_scheduling(mac, iterator->UE, frame, slot, r_pucch, 0);
// TODO properly set the beam index (currently only done for RA)
const int alloc = nr_acknack_scheduling(mac, iterator->UE, frame, slot, 0, r_pucch, 0);
if (alloc<0) {
LOG_D(NR_MAC, "[UE %04x][%4d.%2d] could not find PUCCH for DL DCI\n",
......
......@@ -141,7 +141,7 @@ void nr_preprocessor_phytest(module_id_t module_id,
UE->rnti);
int r_pucch = nr_get_pucch_resource(sched_ctrl->coreset, UE->current_UL_BWP.pucch_Config, CCEIndex);
const int alloc = nr_acknack_scheduling(RC.nrmac[module_id], UE, frame, slot, r_pucch, 0);
const int alloc = nr_acknack_scheduling(RC.nrmac[module_id], UE, frame, slot, 0, r_pucch, 0);
if (alloc < 0) {
LOG_D(MAC,
"%s(): could not find PUCCH for UE %04x@%d.%d\n",
......
......@@ -3093,6 +3093,34 @@ void UL_tti_req_ahead_initialization(gNB_MAC_INST *gNB, int n, int CCid, frame_t
}
}
bool beam_allocation_procedure(NR_beam_info_t *beam_info, int frame, int slot, int beam_index, int slots_per_frame)
{
// if digital beamforming, no restriction
if(!beam_info->beam_allocation)
return true;
const int index = ((frame * slots_per_frame + slot) / beam_info->slots_in_beam_period) % beam_info->beam_allocation_size;
for (int i = 0; i < beam_info->beams_per_period; i++) {
if (beam_info->beam_allocation[i][index] == -1 ||
beam_info->beam_allocation[i][index] == beam_index) {
beam_info->beam_allocation[i][index] = beam_index;
return true;
}
}
return false;
}
void reset_beam_status(NR_beam_info_t *beam_info, int frame, int slot, int beam_index, int slots_per_frame)
{
// if digital beamforming, no restriction
if(!beam_info->beam_allocation)
return;
const int index = ((frame * slots_per_frame + slot) / beam_info->slots_in_beam_period) % beam_info->beam_allocation_size;
for (int i = 0; i < beam_info->beams_per_period; i++) {
if (beam_info->beam_allocation[i][index] == beam_index)
beam_info->beam_allocation[i][index] = -1;
}
}
void send_initial_ul_rrc_message(int rnti, const uint8_t *sdu, sdu_size_t sdu_len, void *data)
{
gNB_MAC_INST *mac = RC.nrmac[0];
......
......@@ -1235,6 +1235,7 @@ int nr_acknack_scheduling(gNB_MAC_INST *mac,
NR_UE_info_t *UE,
frame_t frame,
sub_frame_t slot,
int beam_index,
int r_pucch,
int is_common)
{
......@@ -1331,7 +1332,15 @@ int nr_acknack_scheduling(gNB_MAC_INST *mac,
bwp_start,
bwp_size);
if(!ret) {
LOG_D(NR_MAC, "DL %4d.%2d, UL_ACK %4d.%2d PRB resources for this occasion are already occupied, move to the following occasion\n",
LOG_D(NR_MAC,
"DL %4d.%2d, UL_ACK %4d.%2d PRB resources for this occasion are already occupied, move to the following occasion\n",
frame, slot, pucch_frame, pucch_slot);
continue;
}
ret = beam_allocation_procedure(&mac->beam_info, pucch_frame, pucch_slot, beam_index, n_slots_frame);
if (!ret) {
LOG_D(NR_MAC,
"DL %4d.%2d, UL_ACK %4d.%2d beam resources for this occasion are already occupied, move to the following occasion\n",
frame, slot, pucch_frame, pucch_slot);
continue;
}
......
......@@ -175,6 +175,7 @@ int nr_acknack_scheduling(gNB_MAC_INST *mac,
NR_UE_info_t *UE,
frame_t frameP,
sub_frame_t slotP,
int beam_index,
int r_pucch,
int do_common);
......@@ -419,9 +420,11 @@ int get_mcs_from_bler(const NR_bler_options_t *bler_options,
frame_t frame);
int ul_buffer_index(int frame, int slot, int scs, int size);
void UL_tti_req_ahead_initialization(gNB_MAC_INST *gNB, int n, int CCid, frame_t frameP, int slotP);
bool beam_allocation_procedure(NR_beam_info_t *beam_info, int frame, int slot, int beam_index, int slots_per_frame);
void reset_beam_status(NR_beam_info_t *beam_info, int frame, int slot, int beam_index, int slots_per_frame);
void nr_sr_reporting(gNB_MAC_INST *nrmac, frame_t frameP, sub_frame_t slotP);
size_t dump_mac_stats(gNB_MAC_INST *gNB, char *output, size_t strlen, bool reset_rsrp);
......
......@@ -737,6 +737,14 @@ typedef struct {
uid_allocator_t uid_allocator;
} NR_UEs_t;
typedef struct {
/// list of allocated beams per period
int **beam_allocation;
int slots_in_beam_period;
int beams_per_period;
int beam_allocation_size;
} NR_beam_info_t;
#define UE_iterator(BaSe, VaR) NR_UE_info_t ** VaR##pptr=BaSe, *VaR; while ((VaR=*(VaR##pptr++)))
typedef void (*nr_pp_impl_dl)(module_id_t mod_id,
......@@ -834,8 +842,8 @@ typedef struct gNB_MAC_INST_s {
time_stats_t rx_ulsch_sdu; // include rlc_data_ind
/// processing time of eNB PCH scheduler
time_stats_t schedule_pch;
/// list of allocated beams per period
int16_t *tdd_beam_association;
NR_beam_info_t beam_info;
/// bitmap of DLSCH slots, can hold up to 160 slots
uint64_t dlsch_slot_bitmap[3];
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment