From 5c53035fe280f7b5dbd88bef765e4e206c470427 Mon Sep 17 00:00:00 2001 From: Eric Wild Date: Wed, 21 Feb 2024 19:33:09 +0100 Subject: [PATCH] ms: add sigproclib demod This is basically a fixed version of ttsous ancient branch that can be used instead of the VA. Required config option part of a future patchset. Change-Id: I6558992bd69f18526be5ebe7d424ca00ceb67772 --- GSM/GSMCommon.cpp | 3 + GSM/GSMCommon.h | 5 + Transceiver52M/Resampler.cpp | 2 +- Transceiver52M/ms/ms_rx_lower.cpp | 113 +++++++++++++- Transceiver52M/ms/ms_upper.cpp | 37 +++++ Transceiver52M/sigProcLib.cpp | 244 +++++++++++++++++++++++++++++- Transceiver52M/sigProcLib.h | 14 ++ 7 files changed, 407 insertions(+), 11 deletions(-) diff --git a/GSM/GSMCommon.cpp b/GSM/GSMCommon.cpp index 5e9e4aed..a9e2bb11 100644 --- a/GSM/GSMCommon.cpp +++ b/GSM/GSMCommon.cpp @@ -55,12 +55,15 @@ const BitVector GSM::gEdgeTrainingSequence[] = { }; const BitVector GSM::gDummyBurst("0001111101101110110000010100100111000001001000100000001111100011100010111000101110001010111010010100011001100111001111010011111000100101111101010000"); +const BitVector GSM::gDummyBurstTSC("01110001011100010111000101"); /* 3GPP TS 05.02, section 5.2.7 "Access burst (AB)", synch. sequence bits */ const BitVector GSM::gRACHSynchSequenceTS0("01001011011111111001100110101010001111000"); /* GSM, GMSK (default) */ const BitVector GSM::gRACHSynchSequenceTS1("01010100111110001000011000101111001001101"); /* EGPRS, 8-PSK */ const BitVector GSM::gRACHSynchSequenceTS2("11101111001001110101011000001101101110111"); /* EGPRS, GMSK */ +const BitVector GSM::gSCHSynchSequence("1011100101100010000001000000111100101101010001010111011000011011"); + // |-head-||---------midamble----------------------||--------------data----------------||t| const BitVector GSM::gRACHBurst("0011101001001011011111111001100110101010001111000110111101111110000111001001010110011000"); diff --git a/GSM/GSMCommon.h b/GSM/GSMCommon.h index 48723b40..aa059c28 100644 --- a/GSM/GSMCommon.h +++ b/GSM/GSMCommon.h @@ -52,11 +52,16 @@ extern const BitVector gEdgeTrainingSequence[]; /** C0T0 filler burst, GSM 05.02, 5.2.6 */ extern const BitVector gDummyBurst; +extern const BitVector gDummyBurstTSC; /** Random access burst synch. sequence */ extern const BitVector gRACHSynchSequenceTS0; extern const BitVector gRACHSynchSequenceTS1; extern const BitVector gRACHSynchSequenceTS2; + +/** Synchronization burst sync sequence */ +extern const BitVector gSCHSynchSequence; + /** Random access burst synch. sequence, GSM 05.02 5.2.7 */ extern const BitVector gRACHBurst; diff --git a/Transceiver52M/Resampler.cpp b/Transceiver52M/Resampler.cpp index 910c7ff6..841c3a9b 100644 --- a/Transceiver52M/Resampler.cpp +++ b/Transceiver52M/Resampler.cpp @@ -32,7 +32,7 @@ extern "C" { #define M_PI 3.14159265358979323846264338327f #endif -#define MAX_OUTPUT_LEN 4096 +#define MAX_OUTPUT_LEN 4096*4 using namespace std; diff --git a/Transceiver52M/ms/ms_rx_lower.cpp b/Transceiver52M/ms/ms_rx_lower.cpp index c2adda32..a33c9487 100644 --- a/Transceiver52M/ms/ms_rx_lower.cpp +++ b/Transceiver52M/ms/ms_rx_lower.cpp @@ -19,6 +19,8 @@ * */ +#include "sigProcLib.h" +#include "signalVector.h" #include #include #include @@ -155,12 +157,13 @@ bool ms_trx::handle_sch(bool is_first_sch_acq) auto current_gsm_time = timekeeper.gsmtime(); const auto buf_len = is_first_sch_acq ? SCH_LEN_SPS : ONE_TS_BURST_LEN; const auto which_in_buffer = is_first_sch_acq ? first_sch_buf : burst_copy_buffer; + memset((void *)&sch_acq_buffer[0], 0, sizeof(sch_acq_buffer)); +#if 1 + const auto ts_offset_symb = 0; const auto which_out_buffer = is_first_sch_acq ? sch_acq_buffer : &sch_acq_buffer[40 * 2]; const auto ss = reinterpret_cast *>(which_out_buffer); std::complex channel_imp_resp[CHAN_IMP_RESP_LENGTH * d_OSR]; - int start; - memset((void *)&sch_acq_buffer[0], 0, sizeof(sch_acq_buffer)); convert_and_scale(which_out_buffer, which_in_buffer, buf_len * 2, 1.f / float(rxFullScale)); if (is_first_sch_acq) { float max_corr = 0; @@ -173,9 +176,22 @@ bool ms_trx::handle_sch(bool is_first_sch_acq) detect_burst_nb(&ss[start], &channel_imp_resp[0], 0, sch_demod_bits); auto sch_decode_success = decode_sch(sch_demod_bits, is_first_sch_acq); +#if 0 + auto burst = new signalVector(buf_len, 50); + const auto corr_type = is_first_sch_acq ? sch_detect_type::SCH_DETECT_BUFFER : sch_detect_type::SCH_DETECT_FULL; + struct estim_burst_params ebp; + // scale like uhd, +-2k -> +-32k + convert_and_scale(burst->begin(), which_in_buffer, buf_len * 2, SAMPLE_SCALE_FACTOR); + + auto rv = detectSCHBurst(*burst, 4, 4, corr_type, &ebp); + + int howmuchdelay = ebp.toa * 4; + std::cerr << "ooffs: " << howmuchdelay << " " << std::endl; + std::cerr << "voffs: " << start << " " << sch_decode_success << std::endl; +#endif if (sch_decode_success) { - const auto ts_offset_symb = 0; + const auto ts_offset_symb = 4; if (is_first_sch_acq) { // update ts to first sample in sch buffer, to allow delay calc for current ts first_sch_ts_start = first_sch_buf_rcv_ts + start - (ts_offset_symb * 4) - 1; @@ -190,6 +206,97 @@ bool ms_trx::handle_sch(bool is_first_sch_acq) DBGLG2() << "L SCH : \x1B[31m decode fail \033[0m @ toa:" << start << " " << current_gsm_time.FN() << ":" << current_gsm_time.TN() << std::endl; } +#else + const auto ts_offset_symb = 4; + auto burst = new signalVector(buf_len, 50); + const auto corr_type = is_first_sch_acq ? sch_detect_type::SCH_DETECT_BUFFER : sch_detect_type::SCH_DETECT_FULL; + struct estim_burst_params ebp; + + // scale like uhd, +-2k -> +-32k + convert_and_scale(burst->begin(), which_in_buffer, buf_len * 2, SAMPLE_SCALE_FACTOR); + + auto rv = detectSCHBurst(*burst, 4, 4, corr_type, &ebp); + + int howmuchdelay = ebp.toa * 4; + + if (!rv) { + delete burst; + DBGLG() << "SCH : \x1B[31m detect fail \033[0m NOOOOOOOOOOOOOOOOOO toa:" << ebp.toa << " " + << current_gsm_time.FN() << ":" << current_gsm_time.TN() << std::endl; + return false; + } + + SoftVector *bits; + if (is_first_sch_acq) { + // can't be legit with a buf size spanning _at least_ one SCH but delay that implies partial sch burst + if (howmuchdelay < 0 || (buf_len - howmuchdelay) < ONE_TS_BURST_LEN) { + delete burst; + return false; + } + + struct estim_burst_params ebp2; + // auto sch_chunk = new signalVector(ONE_TS_BURST_LEN, 50); + // auto sch_chunk_start = sch_chunk->begin(); + // memcpy(sch_chunk_start, sch_buf_f.data() + howmuchdelay, sizeof(std::complex) * ONE_TS_BURST_LEN); + + auto delay = delayVector(burst, NULL, -howmuchdelay); + + scaleVector(*delay, (complex)1.0 / ebp.amp); + + auto rv2 = detectSCHBurst(*delay, 4, 4, sch_detect_type::SCH_DETECT_FULL, &ebp2); + DBGLG() << "FIRST SCH : " << (rv2 ? "yes " : " ") << "Timing offset " << ebp2.toa << " symbols" + << std::endl; + + bits = demodAnyBurst(*delay, SCH, 4, &ebp2); + delete delay; + } else { + bits = demodAnyBurst(*burst, SCH, 4, &ebp); + } + + delete burst; + + // clamp to +-1.5 because +-127 softbits scaled by 64 after -0.5 can be at most +-1.5 + clamp_array(bits->begin(), 148, 1.5f); + + float_to_sbit(&bits->begin()[0], (signed char *)&sch_demod_bits[0], 62, 148); + // float_to_sbit(&bits->begin()[106], &data[39], 62, 39); + + if (decode_sch((char *)sch_demod_bits, is_first_sch_acq)) { + auto current_gsm_time_updated = timekeeper.gsmtime(); + if (is_first_sch_acq) { + // update ts to first sample in sch buffer, to allow delay calc for current ts + first_sch_ts_start = first_sch_buf_rcv_ts + howmuchdelay - (ts_offset_symb * 4); + } else { + // continuous sch tracking, only update if off too much + auto diff = [](float x, float y) { return x > y ? x - y : y - x; }; + + auto d = diff(ebp.toa, ts_offset_symb); + if (abs(d) > 0.3) { + if (ebp.toa < ts_offset_symb) + ebp.toa = d; + else + ebp.toa = -d; + temp_ts_corr_offset += ebp.toa * 4; + + DBGLG() << "offs: " << ebp.toa << " " << temp_ts_corr_offset << std::endl; + } + } + + auto a = gsm_sch_check_fn(current_gsm_time_updated.FN() - 1); + auto b = gsm_sch_check_fn(current_gsm_time_updated.FN()); + auto c = gsm_sch_check_fn(current_gsm_time_updated.FN() + 1); + DBGLG() << "L SCH : Timing offset " << rv << " " << ebp.toa << " " << a << b << c << "fn " + << current_gsm_time_updated.FN() << ":" << current_gsm_time_updated.TN() << std::endl; + + delete bits; + return true; + } else { + DBGLG2() << "L SCH : \x1B[31m decode fail \033[0m @ toa:" << ebp.toa << " " << current_gsm_time.FN() + << ":" << current_gsm_time.TN() << std::endl; + } + + delete bits; +#endif return false; } diff --git a/Transceiver52M/ms/ms_upper.cpp b/Transceiver52M/ms/ms_upper.cpp index c5664cd4..2e8bc11f 100644 --- a/Transceiver52M/ms/ms_upper.cpp +++ b/Transceiver52M/ms/ms_upper.cpp @@ -199,6 +199,7 @@ bool upper_trx::pullRadioVector(GSM::Time &wTime, int &RSSI, int &timingOffset) return true; } +#if 1 convert_and_scale(ss, e.burst, ONE_TS_BURST_LEN * 2, 1.f / float(rxFullScale)); pow = energyDetect(sv, 20 * 4 /*sps*/); @@ -232,6 +233,42 @@ bool upper_trx::pullRadioVector(GSM::Time &wTime, int &RSSI, int &timingOffset) // detect_burst(ss, &chan_imp_resp2[0], dummy_burst_start, outbin); #endif } +#else + + // lower layer sch detection offset, easy to verify by just printing the detected value using both the va+sigproc code. + convert_and_scale(ss + 16, e.burst, ONE_TS_BURST_LEN * 2, 15); + + pow = energyDetect(sv, 20 * 4 /*sps*/); + if (pow < -1) { + LOG(ALERT) << "Received empty burst"; + return false; + } + + avg = sqrt(pow); + + /* Detect normal or RACH bursts */ + CorrType type = CorrType::TSC; + struct estim_burst_params ebp; + auto rc = detectAnyBurst(sv, mTSC, 3, 4, type, 48, &ebp); + if (rc > 0) { + type = (CorrType)rc; + } + + if (rc < 0) { + std::cerr << "UR : \x1B[31m rx fail \033[0m @ toa:" << ebp.toa << " " << e.gsmts.FN() << ":" + << e.gsmts.TN() << std::endl; + return false; + } + SoftVector *bits = demodAnyBurst(sv, type, 4, &ebp); + + SoftVector::const_iterator burstItr = bits->begin(); + // invert and fix to +-127 sbits + for (int ii = 0; ii < 148; ii++) { + demodded_softbits[ii] = *burstItr++ > 0.0f ? -127 : 127; + } + delete bits; + +#endif RSSI = (int)floor(20.0 * log10(rxFullScale / avg)); // FIXME: properly handle offset, sch/nb alignment diff? handled by lower anyway... timingOffset = (int)round(0); diff --git a/Transceiver52M/sigProcLib.cpp b/Transceiver52M/sigProcLib.cpp index df87f94b..cb96d1d8 100644 --- a/Transceiver52M/sigProcLib.cpp +++ b/Transceiver52M/sigProcLib.cpp @@ -129,6 +129,8 @@ struct PulseSequence { static CorrelationSequence *gMidambles[] = {NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL}; static CorrelationSequence *gEdgeMidambles[] = {NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL}; static CorrelationSequence *gRACHSequences[] = {NULL,NULL,NULL}; +static CorrelationSequence *gSCHSequence = NULL; +static CorrelationSequence *gDummySequence = NULL; static PulseSequence *GSMPulse1 = NULL; static PulseSequence *GSMPulse4 = NULL; @@ -151,6 +153,12 @@ void sigProcLibDestroy() gRACHSequences[i] = NULL; } + delete gSCHSequence; + gSCHSequence = NULL; + + delete gDummySequence; + gDummySequence = NULL; + delete GMSKRotation1; delete GMSKReverseRotation1; delete GMSKRotation4; @@ -315,6 +323,7 @@ static signalVector *convolve(const signalVector *x, const signalVector *h, append = true; break; case CUSTOM: + // FIXME: x->getstart? if (start < h->size() - 1) { head = h->size() - start; append = true; @@ -1289,6 +1298,77 @@ release: return status; } +static bool generateDummyMidamble(int sps) +{ + bool status = true; + float toa; + complex *data = NULL; + signalVector *autocorr = NULL, *midamble = NULL; + signalVector *midMidamble = NULL, *_midMidamble = NULL; + + delete gDummySequence; + + /* Use middle 16 bits of each TSC. Correlation sequence is not pulse shaped */ + midMidamble = modulateBurst(gDummyBurstTSC.segment(5,16), 0, sps, true); + if (!midMidamble) + return false; + + /* Simulated receive sequence is pulse shaped */ + midamble = modulateBurst(gDummyBurstTSC, 0, sps, false); + if (!midamble) { + status = false; + goto release; + } + + // NOTE: Because ideal TSC 16-bit midamble is 66 symbols into burst, + // the ideal TSC has an + 180 degree phase shift, + // due to the pi/2 frequency shift, that + // needs to be accounted for. + // 26-midamble is 61 symbols into burst, has +90 degree phase shift. + scaleVector(*midMidamble, complex(-1.0, 0.0)); + scaleVector(*midamble, complex(0.0, 1.0)); + + conjugateVector(*midMidamble); + + /* For SSE alignment, reallocate the midamble sequence on 16-byte boundary */ + data = (complex *) convolve_h_alloc(midMidamble->size()); + _midMidamble = new signalVector(data, 0, midMidamble->size(), convolve_h_alloc, free); + _midMidamble->setAligned(true); + midMidamble->copyTo(*_midMidamble); + + autocorr = convolve(midamble, _midMidamble, NULL, NO_DELAY); + if (!autocorr) { + status = false; + goto release; + } + + gDummySequence = new CorrelationSequence; + gDummySequence->sequence = _midMidamble; + gDummySequence->gain = peakDetect(*autocorr, &toa, NULL); + + /* For 1 sps only + * (Half of correlation length - 1) + midpoint of pulse shape + remainder + * 13.5 = (16 / 2 - 1) + 1.5 + (26 - 10) / 2 + */ + if (sps == 1) + gDummySequence->toa = toa - 13.5; + else + gDummySequence->toa = 0; + +release: + delete autocorr; + delete midamble; + delete midMidamble; + + if (!status) { + delete _midMidamble; + free(data); + gDummySequence = NULL; + } + + return status; +} + static CorrelationSequence *generateEdgeMidamble(int tsc) { complex *data = NULL; @@ -1384,6 +1464,69 @@ release: return status; } +bool generateSCHSequence(int sps) +{ + bool status = true; + float toa; + complex *data = NULL; + signalVector *autocorr = NULL; + signalVector *seq0 = NULL, *seq1 = NULL, *_seq1 = NULL; + + delete gSCHSequence; + + seq0 = modulateBurst(gSCHSynchSequence, 0, sps, false); + if (!seq0) + return false; + + seq1 = modulateBurst(gSCHSynchSequence, 0, sps, true); + if (!seq1) { + status = false; + goto release; + } + + conjugateVector(*seq1); + + /* For SSE alignment, reallocate the midamble sequence on 16-byte boundary */ + data = (complex *) convolve_h_alloc(seq1->size()); + _seq1 = new signalVector(data, 0, seq1->size()); + _seq1->setAligned(true); + memcpy(_seq1->begin(), seq1->begin(), seq1->size() * sizeof(complex)); + + autocorr = convolve(seq0, _seq1, autocorr, NO_DELAY); + if (!autocorr) { + status = false; + goto release; + } + + gSCHSequence = new CorrelationSequence; + gSCHSequence->sequence = _seq1; + gSCHSequence->buffer = data; + gSCHSequence->gain = peakDetect(*autocorr, &toa, NULL); + + /* For 1 sps only + * (Half of correlation length - 1) + midpoint of pulse shaping filer + * 20.5 = (64 / 2 - 1) + 1.5 + */ + if (sps == 1) + gSCHSequence->toa = toa - 32.5; + else + gSCHSequence->toa = 0.0; + +release: + delete autocorr; + delete seq0; + delete seq1; + + if (!status) { + delete _seq1; + free(data); + gSCHSequence = NULL; + } + + return status; +} + + /* * Peak-to-average computation +/- range from peak in symbols */ @@ -1441,14 +1584,15 @@ float energyDetect(const signalVector &rxBurst, unsigned windowLength) return energy/windowLength; } -static signalVector *downsampleBurst(const signalVector &burst) +static signalVector *downsampleBurst(const signalVector &burst, int in_len = DOWNSAMPLE_IN_LEN, + int out_len = DOWNSAMPLE_OUT_LEN) { - signalVector in(DOWNSAMPLE_IN_LEN, dnsampler->len()); - signalVector *out = new signalVector(DOWNSAMPLE_OUT_LEN); - burst.copyToSegment(in, 0, DOWNSAMPLE_IN_LEN); + signalVector in(in_len, dnsampler->len()); + // gSCHSequence->sequence->size(), ensure next conv has no realloc + signalVector *out = new signalVector(out_len, 64); + burst.copyToSegment(in, 0, in_len); - if (dnsampler->rotate((float *) in.begin(), DOWNSAMPLE_IN_LEN, - (float *) out->begin(), DOWNSAMPLE_OUT_LEN) < 0) { + if (dnsampler->rotate((float *)in.begin(), in_len, (float *)out->begin(), out_len) < 0) { delete out; out = NULL; } @@ -1469,6 +1613,12 @@ static float computeCI(const signalVector *burst, const CorrelationSequence *syn /* Integer position where the sequence starts */ const int ps = start + 1 - N + (int)roundf(toa); + if(ps < 0) // might be -22 for toa 40 with N=64, if off by a lot during sch ms sync + return 0; + + if (ps + N > burst->size()) + return 0; + /* Estimate Signal power */ S = 0.0f; for (int i=0, j=ps; i<(int)N; i++,j++) @@ -1652,6 +1802,80 @@ static int detectRACHBurst(const signalVector &burst, float threshold, int sps, return rc; } +int detectSCHBurst(signalVector &burst, + float thresh, + int sps, + sch_detect_type state, struct estim_burst_params *ebp) +{ + int rc, start, target, head, tail, len; + complex _amp; + CorrelationSequence *sync; + + if ((sps != 1) && (sps != 4)) + return -1; + + target = 3 + 39 + 64; + + switch (state) { + case sch_detect_type::SCH_DETECT_NARROW: + head = 4; + tail = 4; + break; + case sch_detect_type::SCH_DETECT_BUFFER: + target = 1; + head = 0; + tail = (12 * 8 * 625) / 4; // 12 frames, downsampled /4 to 1 sps + break; + case sch_detect_type::SCH_DETECT_FULL: + default: + head = target - 1; + tail = 39 + 3 + 9; + break; + } + + start = (target - head) * 1 - 1; + len = (head + tail) * 1; + sync = gSCHSequence; + signalVector corr(len); + + signalVector *dec = downsampleBurst(burst, len * 4, len); + rc = detectBurst(*dec, corr, sync, thresh, 1, start, len, ebp); + delete dec; + + if (rc < 0) { + return -1; + } else if (!rc) { + ebp->amp = 0.0f; + ebp->toa = 0.0f; + return 0; + } + + if (state == sch_detect_type::SCH_DETECT_BUFFER) + ebp->toa = ebp->toa - (3 + 39 + 64); + else { + /* Subtract forward search bits from delay */ + ebp->toa = ebp->toa - head; + } + + return rc; +} + +static int detectDummyBurst(const signalVector &burst, float threshold, + int sps, unsigned max_toa, struct estim_burst_params *ebp) +{ + int rc, target, head, tail; + CorrelationSequence *sync; + + target = 3 + 58 + 16 + 5; + head = 10; + tail = 6 + max_toa; + sync = gDummySequence; + + ebp->tsc = 0; + rc = detectGeneralBurst(burst, threshold, sps, target, head, tail, sync, ebp); + return rc; +} + /* * Normal burst detection * @@ -1670,7 +1894,7 @@ static int analyzeTrafficBurst(const signalVector &burst, unsigned tsc, float th return -SIGERR_UNSUPPORTED; target = 3 + 58 + 16 + 5; - head = 6; + head = 10; tail = 6 + max_toa; sync = gMidambles[tsc]; @@ -1719,6 +1943,9 @@ int detectAnyBurst(const signalVector &burst, unsigned tsc, float threshold, case RACH: rc = detectRACHBurst(burst, threshold, sps, max_toa, type == EXT_RACH, ebp); break; + case IDLE: + rc = detectDummyBurst(burst, threshold, sps, max_toa, ebp); + break; default: LOG(ERR) << "Invalid correlation type"; } @@ -1921,6 +2148,9 @@ bool sigProcLibSetup() generateRACHSequence(&gRACHSequences[1], gRACHSynchSequenceTS1, 1); generateRACHSequence(&gRACHSequences[2], gRACHSynchSequenceTS2, 1); + generateSCHSequence(1); + generateDummyMidamble(1); + for (int tsc = 0; tsc < 8; tsc++) { generateMidamble(1, tsc); gEdgeMidambles[tsc] = generateEdgeMidamble(tsc); diff --git a/Transceiver52M/sigProcLib.h b/Transceiver52M/sigProcLib.h index 0c3c7c65..39c8dddc 100644 --- a/Transceiver52M/sigProcLib.h +++ b/Transceiver52M/sigProcLib.h @@ -31,6 +31,7 @@ enum CorrType{ TSC, ///< timeslot should contain a normal burst EXT_RACH, ///< timeslot should contain an extended access burst RACH, ///< timeslot should contain an access burst + SCH, EDGE, ///< timeslot should contain an EDGE burst IDLE ///< timeslot is an idle (or dummy) burst }; @@ -93,6 +94,8 @@ signalVector *generateDummyBurst(int sps, int tn); void scaleVector(signalVector &x, complex scale); +signalVector *delayVector(const signalVector *in, signalVector *out, float delay); + /** Rough energy estimator. @param rxBurst A GSM burst. @@ -133,6 +136,17 @@ int detectAnyBurst(const signalVector &burst, unsigned max_toa, struct estim_burst_params *ebp); +enum class sch_detect_type { + SCH_DETECT_FULL, + SCH_DETECT_NARROW, + SCH_DETECT_BUFFER, +}; + +int detectSCHBurst(signalVector &rxBurst, + float detectThreshold, + int sps, + sch_detect_type state, struct estim_burst_params *ebp); + /** Demodulate burst basde on type and output soft bits */ SoftVector *demodAnyBurst(const signalVector &burst, CorrType type, int sps, struct estim_burst_params *ebp);