Commit b09a281b authored by Raymond Knopp's avatar Raymond Knopp

modifications for TDD and IF4p5 (in particular TDD). Both in RRU and RAU...

modifications for TDD and IF4p5 (in particular TDD). Both in RRU and RAU components. Monolithic is unchanged.
parent 73723a56
......@@ -1677,7 +1677,7 @@ int phy_init_RU(RU_t *ru) {
int p;
int re;
LOG_I(PHY,"Initializing RU signal buffers (if_south %s)\n",ru_if_types[ru->if_south]);
LOG_I(PHY,"Initializing RU signal buffers (if_south %s) nb_tx %d\n",ru_if_types[ru->if_south],ru->nb_tx);
if (ru->if_south <= REMOTE_IF5) { // this means REMOTE_IF5 or LOCAL_RF, so allocate memory for time-domain signals
// Time-domain signals
......@@ -1704,6 +1704,7 @@ int phy_init_RU(RU_t *ru) {
}
if (ru->function != NGFI_RRU_IF5) { // we need to do RX/TX RU processing
LOG_I(PHY,"nb_tx %d\n",ru->nb_tx);
ru->common.rxdata_7_5kHz = (int32_t**)malloc16(ru->nb_rx*sizeof(int32_t*) );
for (i=0;i<ru->nb_rx;i++) {
ru->common.rxdata_7_5kHz[i] = (int32_t*)malloc16_clear( 2*fp->samples_per_tti*2*sizeof(int32_t) );
......
......@@ -6374,6 +6374,8 @@ uint8_t pdcch_alloc2ul_subframe(LTE_DL_FRAME_PARMS *frame_parms,uint8_t n)
else
ul_subframe = ((n+4)%10);
if (subframe_select(frame_parms,ul_subframe) != SF_UL) return(255);
LOG_D(PHY, "subframe %d: PUSCH subframe = %d\n", n, ul_subframe);
return ul_subframe;
}
......
......@@ -72,6 +72,7 @@ void send_IF4p5(RU_t *ru, int frame, int subframe, uint16_t packet_type) {
if (subframe_select(fp,subframe)==SF_S)
nsym=fp->dl_symbols_in_S_subframe;
db_fulllength = 12*fp->N_RB_DL;
db_halflength = (db_fulllength)>>1;
slotoffsetF = 1;//(subframe)*(fp->ofdm_symbol_size)*((fp->Ncp==1) ? 12 : 14) + 1;
......
......@@ -184,8 +184,8 @@ uint8_t phich_subframe2_pusch_subframe(LTE_DL_FRAME_PARMS *frame_parms,uint8_t s
else if (subframe == 1)
pusch_subframe = (7);
else {
LOG_E(PHY, "phich.c: phich_subframe2_pusch_subframe, illegal subframe %d for tdd_config %d\n",
subframe,frame_parms->tdd_config);
AssertFatal(1==0,"phich.c: phich_subframe2_pusch_subframe, illegal subframe %d for tdd_config %d\n",
subframe,frame_parms->tdd_config);
pusch_subframe = (0);
}
......@@ -201,7 +201,7 @@ uint8_t phich_subframe2_pusch_subframe(LTE_DL_FRAME_PARMS *frame_parms,uint8_t s
else if (subframe == 4)
pusch_subframe = (8);
else {
LOG_E(PHY,"phich.c: phich_subframe2_pusch_subframe, illegal subframe %d for tdd_config %d\n",
AssertFatal(1==0,"phich.c: phich_subframe2_pusch_subframe, illegal subframe %d for tdd_config %d\n",
subframe,frame_parms->tdd_config);
pusch_subframe = (0);
}
......@@ -214,7 +214,7 @@ uint8_t phich_subframe2_pusch_subframe(LTE_DL_FRAME_PARMS *frame_parms,uint8_t s
else if (subframe == 3)
pusch_subframe = (7);
else {
LOG_E(PHY,"phich.c: phich_subframe2_pusch_subframe, illegal subframe %d for tdd_config %d\n",
AssertFatal(1==0,"phich.c: phich_subframe2_pusch_subframe, illegal subframe %d for tdd_config %d\n",
subframe,frame_parms->tdd_config);
pusch_subframe = (0);
}
......@@ -227,7 +227,7 @@ uint8_t phich_subframe2_pusch_subframe(LTE_DL_FRAME_PARMS *frame_parms,uint8_t s
} else if (subframe==0)
pusch_subframe = (4);
else {
LOG_E(PHY,"phich.c: phich_subframe2_pusch_subframe, illegal subframe %d for tdd_config %d\n",
AssertFatal(1==0,"phich.c: phich_subframe2_pusch_subframe, illegal subframe %d for tdd_config %d\n",
subframe,frame_parms->tdd_config);
pusch_subframe = (0);
}
......@@ -238,7 +238,7 @@ uint8_t phich_subframe2_pusch_subframe(LTE_DL_FRAME_PARMS *frame_parms,uint8_t s
if ( (subframe == 8) || (subframe == 9) ) {
pusch_subframe = (subframe-6);
} else {
LOG_E(PHY,"phich.c: phich_subframe2_pusch_subframe, illegal subframe %d for tdd_config %d\n",
AssertFatal(1==0,"phich.c: phich_subframe2_pusch_subframe, illegal subframe %d for tdd_config %d\n",
subframe,frame_parms->tdd_config);
pusch_subframe = (0);
}
......@@ -249,7 +249,7 @@ uint8_t phich_subframe2_pusch_subframe(LTE_DL_FRAME_PARMS *frame_parms,uint8_t s
if (subframe == 8) {
pusch_subframe = (2);
} else {
LOG_E(PHY,"phich.c: phich_subframe2_pusch_subframe, illegal subframe %d for tdd_config %d\n",
AssertFatal(1==0,"phich.c: phich_subframe2_pusch_subframe, illegal subframe %d for tdd_config %d\n",
subframe,frame_parms->tdd_config);
pusch_subframe = (0);
}
......@@ -268,7 +268,7 @@ uint8_t phich_subframe2_pusch_subframe(LTE_DL_FRAME_PARMS *frame_parms,uint8_t s
} else if (subframe == 5) {
pusch_subframe = (8);
} else {
LOG_E(PHY,"phich.c: phich_subframe2_pusch_subframe, illegal subframe %d for tdd_config %d\n",
AssertFatal(1==0,"phich.c: phich_subframe2_pusch_subframe, illegal subframe %d for tdd_config %d\n",
subframe,frame_parms->tdd_config);
pusch_subframe = (0);
}
......@@ -276,7 +276,7 @@ uint8_t phich_subframe2_pusch_subframe(LTE_DL_FRAME_PARMS *frame_parms,uint8_t s
break;
default:
LOG_E(PHY, "no implementation for TDD UL/DL-config = %d!\n", frame_parms->tdd_config);
AssertFatal(1==0, "no implementation for TDD UL/DL-config = %d!\n", frame_parms->tdd_config);
pusch_subframe = (0);
}
......@@ -560,7 +560,7 @@ void generate_phich(LTE_DL_FRAME_PARMS *frame_parms,
break;
default:
LOG_E(PHY,"phich_coding.c: Illegal PHICH Number\n");
AssertFatal(1==0,"phich_coding.c: Illegal PHICH Number\n");
} // nseq_PHICH
}
......@@ -858,7 +858,7 @@ void generate_phich(LTE_DL_FRAME_PARMS *frame_parms,
break;
default:
LOG_E(PHY,"phich_coding.c: Illegal PHICH Number\n");
AssertFatal(1==0,"phich_coding.c: Illegal PHICH Number\n");
}
......@@ -1255,7 +1255,7 @@ void rx_phich(PHY_VARS_UE *ue,
break;
default:
LOG_E(PHY,"phich_coding.c: Illegal PHICH Number\n");
AssertFatal(1==0,"phich_coding.c: Illegal PHICH Number\n");
} // nseq_PHICH
}
......@@ -1342,7 +1342,7 @@ void rx_phich(PHY_VARS_UE *ue,
break;
default:
LOG_E(PHY,"phich_coding.c: Illegal PHICH Number\n");
AssertFatal(1==0,"phich_coding.c: Illegal PHICH Number\n");
}
}
......@@ -1527,9 +1527,11 @@ void generate_phich_top(PHY_VARS_eNB *eNB,
if (frame_parms->Ncp == 1)
NSF_PHICH = 2;
pusch_frame = phich_frame2_pusch_frame(frame_parms,proc->frame_tx,subframe);
pusch_subframe = phich_subframe2_pusch_subframe(frame_parms,subframe);
harq_pid = subframe2harq_pid(frame_parms,pusch_frame,pusch_subframe);
if (eNB->phich_vars[subframe&1].num_hi > 0) {
pusch_frame = phich_frame2_pusch_frame(frame_parms,proc->frame_tx,subframe);
pusch_subframe = phich_subframe2_pusch_subframe(frame_parms,subframe);
harq_pid = subframe2harq_pid(frame_parms,pusch_frame,pusch_subframe);
}
for (i=0; i<eNB->phich_vars[subframe&1].num_hi; i++) {
......
......@@ -575,7 +575,7 @@ int is_prach_subframe0(LTE_DL_FRAME_PARMS *frame_parms,uint8_t prach_ConfigIndex
(((subframe>4)&&(t1_ra==1))))) { // PRACH is in 2nd half-frame
if ((prach_ConfigIndex<48) && // PRACH only in normal UL subframe
(((subframe%5)-2)==t2_ra)) prach_mask=1;
else if ((((subframe%5)-1)==t2_ra)) prach_mask=1; // PRACH can be in UpPTS
else if ((prach_ConfigIndex>47) && (((subframe%5)-1)==t2_ra)) prach_mask=1; // PRACH can be in UpPTS
}
}
......
......@@ -1498,6 +1498,10 @@ typedef struct RRU_config_s {
uint8_t num_bands;
/// EUTRA band list configured in RRU
uint8_t band_list[MAX_BANDS_PER_RRU];
/// TDD configuration (0-6)
uint8_t tdd_config[MAX_BANDS_PER_RRU];
/// TDD special subframe configuration (0-10)
uint8_t tdd_config_S[MAX_BANDS_PER_RRU];
/// TX frequency
uint32_t tx_freq[MAX_BANDS_PER_RRU];
/// RX frequency
......
......@@ -571,7 +571,7 @@ void schedule_response(Sched_Rsp_t *Sched_INFO)
frame_t frame = Sched_INFO->frame;
sub_frame_t subframe = Sched_INFO->subframe;
LTE_DL_FRAME_PARMS *fp;
int ul_subframe;
uint8_t ul_subframe;
int ul_frame;
int harq_pid;
LTE_UL_eNB_HARQ_t *ulsch_harq;
......@@ -591,7 +591,7 @@ void schedule_response(Sched_Rsp_t *Sched_INFO)
uint8_t number_dl_pdu = DL_req->dl_config_request_body.number_pdu;
uint8_t number_hi_dci0_pdu = HI_DCI0_req->hi_dci0_request_body.number_of_dci+HI_DCI0_req->hi_dci0_request_body.number_of_hi;
uint8_t number_ul_pdu = UL_req->ul_config_request_body.number_of_pdus;
uint8_t number_ul_pdu = UL_req!=NULL ? UL_req->ul_config_request_body.number_of_pdus : 0;
nfapi_dl_config_request_pdu_t *dl_config_pdu;
nfapi_hi_dci0_request_pdu_t *hi_dci0_req_pdu;
......@@ -603,12 +603,13 @@ void schedule_response(Sched_Rsp_t *Sched_INFO)
eNB->pdcch_vars[subframe&1].num_dci = 0;
eNB->phich_vars[subframe&1].num_hi = 0;
LOG_D(PHY,"NFAPI: Frame %d, Subframe %d: received %d dl_pdu, %d tx_req, %d hi_dci0_config_req, %d UL_config \n",
frame,subframe,number_dl_pdu,TX_req->tx_request_body.number_of_pdus,number_hi_dci0_pdu,number_ul_pdu);
LOG_D(PHY,"NFAPI: Frame %d, Subframe %d (ul_subframe %d): received %d dl_pdu, %d tx_req, %d hi_dci0_config_req, %d UL_config \n",
frame,subframe,ul_subframe,number_dl_pdu,TX_req->tx_request_body.number_of_pdus,number_hi_dci0_pdu,number_ul_pdu);
if ((subframe_select(fp,ul_subframe)==SF_UL) ||
(fp->frame_type == FDD)) {
if (ul_subframe<10) { // This means that there is an ul_subframe that can be configured here
LOG_D(PHY,"NFAPI: Clearing dci allocations for potential UL\n");
harq_pid = subframe2harq_pid(fp,ul_frame,ul_subframe);
// clear DCI allocation maps for new subframe
......
......@@ -437,33 +437,33 @@ void phy_procedures_eNB_TX(PHY_VARS_eNB *eNB,
}
/* save old HARQ information needed for PHICH generation */
for (i=0; i<NUMBER_OF_UE_MAX; i++) {
harq_pid = subframe2harq_pid(fp,ul_frame,ul_subframe);
if (eNB->ulsch[i]) {
ulsch_harq = eNB->ulsch[i]->harq_processes[harq_pid];
/* Store first_rb and n_DMRS for correct PHICH generation below.
* For PHICH generation we need "old" values of last scheduling
* for this HARQ process. 'generate_eNB_dlsch_params' below will
* overwrite first_rb and n_DMRS and 'generate_phich_top', done
* after 'generate_eNB_dlsch_params', would use the "new" values
* instead of the "old" ones.
*
* This has been tested for FDD only, may be wrong for TDD.
*
* TODO: maybe we should restructure the code to be sure it
* is done correctly. The main concern is if the code
* changes and first_rb and n_DMRS are modified before
* we reach here, then the PHICH processing will be wrong,
* using wrong first_rb and n_DMRS values to compute
* ngroup_PHICH and nseq_PHICH.
*
* TODO: check if that works with TDD.
*/
if ((subframe_select(fp,ul_subframe)==SF_UL) ||
(fp->frame_type == FDD)) {
ulsch_harq->previous_first_rb = ulsch_harq->first_rb;
ulsch_harq->previous_n_DMRS = ulsch_harq->n_DMRS;
if (ul_subframe < 10) { // This means that there is a potential UL subframe that will be scheduled here
for (i=0; i<NUMBER_OF_UE_MAX; i++) {
harq_pid = subframe2harq_pid(fp,ul_frame,ul_subframe);
if (eNB->ulsch[i]) {
ulsch_harq = eNB->ulsch[i]->harq_processes[harq_pid];
/* Store first_rb and n_DMRS for correct PHICH generation below.
* For PHICH generation we need "old" values of last scheduling
* for this HARQ process. 'generate_eNB_dlsch_params' below will
* overwrite first_rb and n_DMRS and 'generate_phich_top', done
* after 'generate_eNB_dlsch_params', would use the "new" values
* instead of the "old" ones.
*
* This has been tested for FDD only, may be wrong for TDD.
*
* TODO: maybe we should restructure the code to be sure it
* is done correctly. The main concern is if the code
* changes and first_rb and n_DMRS are modified before
* we reach here, then the PHICH processing will be wrong,
* using wrong first_rb and n_DMRS values to compute
* ngroup_PHICH and nseq_PHICH.
*
* TODO: check if that works with TDD.
*/
ulsch_harq->previous_first_rb = ulsch_harq->first_rb;
ulsch_harq->previous_n_DMRS = ulsch_harq->n_DMRS;
}
}
}
......
......@@ -3583,7 +3583,6 @@ void ue_dlsch_procedures(PHY_VARS_UE *ue,
}
}
if (abstraction_flag == 0) {
// start turbo decode for CW 0
dlsch0->harq_processes[harq_pid]->G = get_G(&ue->frame_parms,
......@@ -3720,17 +3719,7 @@ void ue_dlsch_procedures(PHY_VARS_UE *ue,
}
LOG_D(PHY," ------ end turbo decoder for AbsSubframe %d.%d ------ \n", frame_rx, subframe_rx);
}
else {
LOG_D(PHY,"Calling dlsch_decoding_emul ...\n");
#ifdef PHY_ABSTRACTION
ret = dlsch_decoding_emul(ue,
subframe_rx,
pdsch,
eNB_id);
#endif
}
// Check CRC for CW 0
......
......@@ -574,14 +574,10 @@ void fh_if5_north_asynch_in(RU_t *ru,int *frame,int *subframe) {
proc->first_tx = 0;
}
else {
if (subframe_tx != *subframe) {
LOG_E(PHY,"subframe_tx %d is not what we expect %d\n",subframe_tx,*subframe);
exit_fun("Exiting");
}
if (frame_tx != *frame) {
LOG_E(PHY,"frame_tx %d is not what we expect %d\n",frame_tx,*frame);
exit_fun("Exiting");
}
AssertFatal(subframe_tx == *subframe,
"subframe_tx %d is not what we expect %d\n",subframe_tx,*subframe);
AssertFatal(frame_tx == *frame,
"frame_tx %d is not what we expect %d\n",frame_tx,*frame);
}
}
......@@ -596,32 +592,26 @@ void fh_if4p5_north_asynch_in(RU_t *ru,int *frame,int *subframe) {
symbol_number = 0;
symbol_mask = 0;
symbol_mask_full = (1<<fp->symbols_per_tti)-1;
symbol_mask_full = ((subframe_select(fp,*subframe) == SF_S) ? (1<<fp->dl_symbols_in_S_subframe) : (1<<fp->symbols_per_tti))-1;
do {
recv_IF4p5(ru, &frame_tx, &subframe_tx, &packet_type, &symbol_number);
LOG_D(PHY,"subframe %d (%d): frame %d, subframe %d, symbol %d\n",
*subframe,subframe_select(fp,*subframe),frame_tx,subframe_tx,symbol_number);
if (proc->first_tx != 0) {
*frame = frame_tx;
*subframe = subframe_tx;
proc->first_tx = 0;
}
else {
if (frame_tx != *frame) {
LOG_E(PHY,"frame_tx %d is not what we expect %d\n",frame_tx,*frame);
exit_fun("Exiting");
}
if (subframe_tx != *subframe) {
LOG_E(PHY,"subframe_tx %d is not what we expect %d\n",subframe_tx,*subframe);
exit_fun("Exiting");
}
AssertFatal(frame_tx == *frame,
"frame_tx %d is not what we expect %d\n",frame_tx,*frame);
AssertFatal(subframe_tx == *subframe,
"subframe_tx %d is not what we expect %d\n",subframe_tx,*subframe);
}
if (packet_type == IF4p5_PDLFFT) {
symbol_mask = symbol_mask | (1<<symbol_number);
}
else {
LOG_E(PHY,"Illegal IF4p5 packet type (should only be IF4p5_PDLFFT%d\n",packet_type);
exit_fun("Exiting");
}
else AssertFatal(1==0,"Illegal IF4p5 packet type (should only be IF4p5_PDLFFT%d\n",packet_type);
} while (symbol_mask != symbol_mask_full);
proc->subframe_tx = subframe_tx;
......@@ -703,8 +693,8 @@ void rx_rf(RU_t *ru,int *frame,int *subframe) {
proc->timestamp_rx = ts-ru->ts_offset;
if (rxs != fp->samples_per_tti)
LOG_E(PHY,"rx_rf: Asked for %d samples, got %d from USRP\n",fp->samples_per_tti,rxs);
AssertFatal(rxs == fp->samples_per_tti,
"rx_rf: Asked for %d samples, got %d from USRP\n",fp->samples_per_tti,rxs);
if (proc->first_rx == 1) {
ru->ts_offset = proc->timestamp_rx;
......@@ -823,11 +813,8 @@ void tx_rf(RU_t *ru) {
VCD_SIGNAL_DUMPER_DUMP_FUNCTION_BY_NAME( VCD_SIGNAL_DUMPER_FUNCTIONS_TRX_WRITE, 0 );
if (txs != fp->samples_per_tti) {
LOG_E(PHY,"TX : Timeout (sent %d/%d)\n",txs, fp->samples_per_tti);
exit_fun( "problem transmitting samples" );
}
AssertFatal(txs == siglen,"TX : Timeout (sent %d/%d)\n",txs, siglen);
}
}
......@@ -1461,7 +1448,6 @@ static void* ru_thread( void* param ) {
// If this proc is to provide synchronization, do so
wakeup_slaves(proc);
LOG_D(PHY,"RU %d/%d frame_tx %d, subframe_tx %d\n",0,ru->idx,proc->frame_tx,proc->subframe_tx);
// wakeup all eNB processes waiting for this RU
if (ru->num_eNB>0) wakeup_eNBs(ru);
......@@ -1746,6 +1732,10 @@ void configure_ru(int idx,
AssertFatal((ret=check_capabilities(ru,capabilities)) == 0,
"Cannot configure RRU %d, check_capabilities returned %d\n", idx,ret);
// take antenna capabilities of RRU
ru->nb_tx = capabilities->nb_tx[0];
ru->nb_rx = capabilities->nb_rx[0];
// Pass configuration to RRU
LOG_I(PHY, "Using %s fronthaul (%d), band %d \n",ru_if_formats[ru->if_south],ru->if_south,ru->frame_parms.eutra_band);
// wait for configuration
......@@ -1754,6 +1744,8 @@ void configure_ru(int idx,
config->band_list[0] = ru->frame_parms.eutra_band;
config->tx_freq[0] = ru->frame_parms.dl_CarrierFreq;
config->rx_freq[0] = ru->frame_parms.ul_CarrierFreq;
config->tdd_config[0] = ru->frame_parms.tdd_config;
config->tdd_config_S[0] = ru->frame_parms.tdd_config_S;
config->att_tx[0] = ru->att_tx;
config->att_rx[0] = ru->att_rx;
config->N_RB_DL[0] = ru->frame_parms.N_RB_DL;
......@@ -1773,10 +1765,6 @@ void configure_ru(int idx,
}
#endif
}
// take antenna capabilities of RRU
ru->nb_tx = capabilities->nb_tx[0];
ru->nb_rx = capabilities->nb_rx[0];
init_frame_parms(&ru->frame_parms,1);
phy_init_RU(ru);
......@@ -1791,6 +1779,13 @@ void configure_rru(int idx,
ru->frame_parms.eutra_band = config->band_list[0];
ru->frame_parms.dl_CarrierFreq = config->tx_freq[0];
ru->frame_parms.ul_CarrierFreq = config->rx_freq[0];
if (ru->frame_parms.dl_CarrierFreq == ru->frame_parms.ul_CarrierFreq) {
ru->frame_parms.frame_type = TDD;
ru->frame_parms.tdd_config = config->tdd_config[0];
ru->frame_parms.tdd_config_S = config->tdd_config_S[0];
}
else
ru->frame_parms.frame_type = FDD;
ru->att_tx = config->att_tx[0];
ru->att_rx = config->att_rx[0];
ru->frame_parms.N_RB_DL = config->N_RB_DL[0];
......@@ -1867,7 +1862,7 @@ void init_RU(char *rf_config_file) {
// read in configuration file)
printf("configuring RU from file\n");
RCconfig_RU();
printf("number of L1 instances %d, number of RU %d\n",RC.nb_L1_inst,RC.nb_RU);
LOG_I(PHY,"number of L1 instances %d, number of RU %d, number of CPU cores %d\n",RC.nb_L1_inst,RC.nb_RU,get_nprocs());
for (i=0;i<RC.nb_L1_inst;i++)
for (CC_id=0;CC_id<RC.nb_CC[i];CC_id++) RC.eNB[i][CC_id]->num_RU=0;
......@@ -1927,8 +1922,8 @@ void init_RU(char *rf_config_file) {
ru->fh_north_out = fh_if4p5_north_out; // send_IF4p5 on reception
ru->fh_south_out = tx_rf; // send output to RF
ru->fh_north_asynch_in = fh_if4p5_north_asynch_in; // TX packets come asynchronously
ru->feprx = (get_nprocs()<=4) ? fep_full :ru_fep_full_2thread; // RX DFTs
ru->feptx_ofdm = (get_nprocs()<=4) ? feptx_ofdm : feptx_ofdm_2thread; // this is fep with idft only (no precoding in RRU)
ru->feprx = (get_nprocs()<=2) ? fep_full :ru_fep_full_2thread; // RX DFTs
ru->feptx_ofdm = (get_nprocs()<=2) ? feptx_ofdm : feptx_ofdm_2thread; // this is fep with idft only (no precoding in RRU)
ru->feptx_prec = NULL;
ru->start_if = start_if; // need to start the if interface for if4p5
ru->ifdevice.host_type = RRU_HOST;
......
......@@ -785,7 +785,7 @@ void get_simulation_options(int argc, char *argv[])
if (RC.config_file_name != NULL) {
/* Read eNB configuration file */
RCConfig(RC.config_file_name);
RCConfig();
printf("returned with %d eNBs, %d rus\n",RC.nb_inst,RC.nb_RU);
oai_emulation.info.nb_enb_local = RC.nb_inst;
oai_emulation.info.nb_ru_local = RC.nb_RU;
......
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