Skip to content

Commit fc28f48

Browse files
committed
phy: provide cfo estimation
The port channel estimator now estimates the CFO and takes it into account in the estimation of the channel coefficients.
1 parent e976951 commit fc28f48

File tree

6 files changed

+308
-38
lines changed

6 files changed

+308
-38
lines changed

include/srsran/phy/upper/channel_estimation.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ class channel_estimate
101101
snr.resize(nof_paths);
102102
time_alignment.reserve(MAX_TX_RX_PATHS);
103103
time_alignment.resize(nof_paths);
104+
cfo.reserve(MAX_TX_RX_PATHS);
105+
cfo.resize(nof_paths);
104106
}
105107

106108
/// Default destructor
@@ -177,6 +179,12 @@ class channel_estimate
177179
return time_alignment[path_to_index(rx_port, tx_layer)];
178180
}
179181

182+
/// Returns the carrier frequency offset in hertz estimated for the given Rx port and Tx layer.
183+
optional<float> get_cfo_Hz(unsigned rx_port, unsigned tx_layer = 0) const
184+
{
185+
return cfo[path_to_index(rx_port, tx_layer)];
186+
}
187+
180188
/// \brief Returns a read-write view to the RE channel estimates of the path between the given Rx port and Tx layer.
181189
///
182190
/// The view is represented as a vector indexed by i) subcarriers and ii) OFDM symbols.
@@ -280,6 +288,12 @@ class channel_estimate
280288
time_alignment[path_to_index(rx_port, tx_layer)] = ta;
281289
}
282290

291+
/// Sets the estimated carrier frequency offset in hertz for the path between the given Rx port and Tx layer.
292+
void set_cfo_Hz(optional<float> new_cfo, unsigned rx_port, unsigned tx_layer = 0)
293+
{
294+
cfo[path_to_index(rx_port, tx_layer)] = new_cfo;
295+
}
296+
283297
/// Sets the channel estimate for the resource element at the given coordinates.
284298
void set_ch_estimate(cf_t ce_val, unsigned subcarrier, unsigned symbol, unsigned rx_port = 0, unsigned tx_layer = 0)
285299
{
@@ -304,6 +318,7 @@ class channel_estimate
304318
epre.resize(nof_paths);
305319
rsrp.resize(nof_paths);
306320
snr.resize(nof_paths);
321+
cfo.resize(nof_paths);
307322

308323
unsigned nof_res = nof_paths * nof_subcarriers * nof_symbols;
309324
srsran_assert(nof_res <= MAX_BUFFER_SIZE,
@@ -362,6 +377,9 @@ class channel_estimate
362377

363378
/// Estimated time alignment.
364379
std::vector<phy_time_unit> time_alignment;
380+
381+
/// Estimated CFO.
382+
std::vector<optional<float>> cfo;
365383
///@}
366384

367385
/// \brief Container for channel estimates.

lib/phy/upper/signal_processors/port_channel_estimator_average_impl.cpp

Lines changed: 148 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,37 @@ static unsigned extract_layer_hop_rx_pilots(dmrs_symbol_list&
142142
unsigned hop,
143143
unsigned i_layer);
144144

145+
/// \brief Preprocesses the pilots and computes the CFO.
146+
///
147+
/// For the current hop, the function does the following:
148+
/// - matches the received pilots with the expected ones (element-wise multiplication with complex conjugate);
149+
/// - estimates the CFO (if the number of OFDM symbols with pilots is at least 2);
150+
/// - compensates the CFO for all pilots;
151+
/// - accumulates all the matched, CFO-compensated received pilots.
152+
/// \param[out] pilots_lse Result of the accumulation of all matched pilots.
153+
/// \param[out] pilot_products Helper buffer for internal computations (same size as pilots_lse).
154+
/// \param[in] rx_pilots Received pilots.
155+
/// \param[in] pilots Transmitted pilots.
156+
/// \param[in] scs Subcarrier spacing.
157+
/// \param[in] cp_cum_duration Cumulative duration of all CPs in the slot.
158+
/// \param[in] first_hop_symbol Index of the first OFDM symbol of the current hop, within the slot.
159+
/// \param[in] last_hop_symbol Index of the last OFDM symbol of the current hop (not included), within the slot.
160+
/// \param[in] hop_offset Number of OFDM symbols carrying DM-RS in the previous hop.
161+
/// \param[in] i_layer The considered transmission layer.
162+
/// \return A contribution to the EPRE and CFO estimates. CFO is empty if the hop has only one OFDM symbol carrying
163+
/// DM-RS.
164+
static std::pair<float, optional<float>> preprocess_pilots_and_cfo(span<cf_t> pilots_lse,
165+
span<cf_t> pilot_products,
166+
const dmrs_symbol_list& rx_pilots,
167+
const dmrs_symbol_list& pilots,
168+
const bounded_bitset<MAX_NSYMB_PER_SLOT>& dmrs_mask,
169+
const subcarrier_spacing& scs,
170+
span<const float> cp_cum_duration,
171+
unsigned first_hop_symbol,
172+
unsigned last_hop_symbol,
173+
unsigned hop_offset,
174+
unsigned i_layer);
175+
145176
/// \brief Estimates the noise energy of one hop.
146177
/// \param[in] pilots DM-RS pilots.
147178
/// \param[in] rx_pilots Received samples corresponding to DM-RS pilots.
@@ -199,26 +230,47 @@ void port_channel_estimator_average_impl::compute(channel_estimate& es
199230
// Prepare symbol destination.
200231
rx_pilots.resize(symbols_size);
201232

233+
// Compute the cumulative duration of all CPs for the given subcarrier spacing.
234+
initialize_cp_cum_duration(cfg.cp, cfg.scs);
235+
202236
// For each layer...
203237
for (unsigned i_layer = 0, nof_tx_layers = cfg.dmrs_pattern.size(); i_layer != nof_tx_layers; ++i_layer) {
204238
rsrp = 0;
205239
epre = 0;
206240
noise_var = 0;
207241
time_alignment_s = 0;
242+
cfo_normalized = nullopt;
208243

209-
// compute_layer_hop updates rsrp, epre, niose_var and time_alignment_s.
210-
compute_layer_hop(estimate, grid, port, pilots, cfg, 0, i_layer);
244+
// compute_layer_hop updates rsrp, epre, niose_var, time_alignment_s, and cfo_normalized.
245+
compute_layer_hop(estimate, grid, port, pilots, cfg, /*hop=*/0, i_layer);
211246
if (cfg.dmrs_pattern[i_layer].hopping_symbol_index.has_value()) {
212-
compute_layer_hop(estimate, grid, port, pilots, cfg, 1, i_layer);
247+
compute_layer_hop(estimate, grid, port, pilots, cfg, /*hop=*/1, i_layer);
213248
time_alignment_s /= 2.0F;
214249
}
215250

251+
if (cfo_normalized.has_value()) {
252+
// Apply CFO to the estimated channel.
253+
float cfo = cfo_normalized.value();
254+
for (unsigned i_symbol = cfg.first_symbol, last_symbol = cfg.first_symbol + cfg.nof_symbols;
255+
i_symbol != last_symbol;
256+
++i_symbol) {
257+
span<cf_t> symbol_ch_estimate = estimate.get_symbol_ch_estimate(i_symbol);
258+
srsvec::sc_prod(symbol_ch_estimate,
259+
std::polar(1.0f, TWOPI * (i_symbol + cp_cum_duration[i_symbol]) * cfo),
260+
symbol_ch_estimate);
261+
}
262+
}
263+
216264
rsrp /= static_cast<float>(nof_dmrs_pilots);
217265
epre /= static_cast<float>(nof_dmrs_pilots);
218266

219267
estimate.set_rsrp(rsrp, port, i_layer);
220268
estimate.set_epre(epre, port, i_layer);
221269
estimate.set_time_alignment(phy_time_unit::from_seconds(time_alignment_s), port, i_layer);
270+
estimate.set_cfo_Hz(
271+
cfo_normalized.has_value() ? optional<float>(cfo_normalized.value() * scs_to_khz(cfg.scs) * 1000) : nullopt,
272+
port,
273+
i_layer);
222274

223275
noise_var /= static_cast<float>(nof_dmrs_pilots - 1);
224276

@@ -271,19 +323,27 @@ void port_channel_estimator_average_impl::compute_layer_hop(srsran::channel_esti
271323

272324
span<cf_t> pilot_products = span<cf_t>(aux_pilot_products).first(pilots.size().nof_subc);
273325
span<cf_t> pilots_lse = span<cf_t>(aux_pilots_lse).subspan(MAX_V_PILOTS, pilots.size().nof_subc);
274-
srsvec::prod_conj(rx_pilots.get_symbol(0, i_layer), pilots.get_symbol(hop_offset, i_layer), pilots_lse);
275326

276-
epre += std::real(srsvec::dot_prod(rx_pilots.get_symbol(0, i_layer), rx_pilots.get_symbol(0, i_layer)));
277-
278-
// Accumulate all symbols frequency domain response.
279-
for (unsigned i_dmrs_symbol = 1; i_dmrs_symbol != nof_dmrs_symbols; ++i_dmrs_symbol) {
280-
srsvec::prod_conj(rx_pilots.get_symbol(i_dmrs_symbol, i_layer),
281-
pilots.get_symbol(hop_offset + i_dmrs_symbol, i_layer),
282-
pilot_products);
283-
srsvec::add(pilots_lse, pilot_products, pilots_lse);
284-
285-
epre += std::real(
286-
srsvec::dot_prod(rx_pilots.get_symbol(i_dmrs_symbol, i_layer), rx_pilots.get_symbol(i_dmrs_symbol, i_layer)));
327+
std::pair</*epre*/ float, /*cfo*/ optional<float>> hop_results = preprocess_pilots_and_cfo(pilots_lse,
328+
pilot_products,
329+
rx_pilots,
330+
pilots,
331+
pattern.symbols,
332+
cfg.scs,
333+
cp_cum_duration,
334+
first_symbol,
335+
last_symbol,
336+
hop_offset,
337+
i_layer);
338+
epre += hop_results.first;
339+
340+
if (hop_results.second.has_value()) {
341+
float cfo_hop = hop_results.second.value();
342+
if (cfo_normalized.has_value()) {
343+
cfo_normalized = (cfo_normalized.value() + cfo_hop) / 2;
344+
} else {
345+
cfo_normalized = cfo_hop;
346+
}
287347
}
288348

289349
// Average and apply DM-RS-to-data gain.
@@ -378,6 +438,79 @@ static unsigned extract_layer_hop_rx_pilots(dmrs_symbol_list&
378438
return dmrs_symbol_index;
379439
}
380440

441+
void port_channel_estimator_average_impl::initialize_cp_cum_duration(cyclic_prefix cp, subcarrier_spacing scs)
442+
{
443+
unsigned nof_symbols_slot = get_nsymb_per_slot(cp);
444+
cp_cum_duration = span<float>(cp_cum_help).first(nof_symbols_slot);
445+
446+
// Compute cumulative duration of CPs.
447+
cp_cum_duration[0] = cp.get_length(0, scs).to_seconds() * scs_to_khz(scs) * 1000;
448+
for (unsigned i_cp = 1; i_cp != nof_symbols_slot; ++i_cp) {
449+
cp_cum_duration[i_cp] = cp_cum_duration[i_cp - 1] + cp.get_length(i_cp, scs).to_seconds() * scs_to_khz(scs) * 1000;
450+
}
451+
}
452+
453+
static std::pair<float, optional<float>> preprocess_pilots_and_cfo(span<cf_t> pilots_lse,
454+
span<cf_t> pilot_products,
455+
const dmrs_symbol_list& rx_pilots,
456+
const dmrs_symbol_list& pilots,
457+
const bounded_bitset<MAX_NSYMB_PER_SLOT>& dmrs_mask,
458+
const subcarrier_spacing& scs,
459+
span<const float> cp_cum_duration,
460+
unsigned first_hop_symbol,
461+
unsigned last_hop_symbol,
462+
unsigned hop_offset,
463+
unsigned i_layer)
464+
{
465+
// Number of OFDM symbols carrying DM-RS in the current hop.
466+
unsigned nof_dmrs_symbols = dmrs_mask.slice(first_hop_symbol, last_hop_symbol).count();
467+
468+
// Match received and transmitted pilots in the first DM-RS symbol and compute EPRE contribution.
469+
srsvec::prod_conj(rx_pilots.get_symbol(0, i_layer), pilots.get_symbol(hop_offset, i_layer), pilots_lse);
470+
float epre = std::real(srsvec::dot_prod(rx_pilots.get_symbol(0, i_layer), rx_pilots.get_symbol(0, i_layer)));
471+
472+
if (nof_dmrs_symbols == 1) {
473+
return {epre, nullopt};
474+
}
475+
476+
// Match received and transmitted pilots in the second DM-RS symbol and compute EPRE contribution.
477+
srsvec::prod_conj(rx_pilots.get_symbol(1, i_layer), pilots.get_symbol(hop_offset + 1, i_layer), pilot_products);
478+
epre += std::real(srsvec::dot_prod(rx_pilots.get_symbol(1, i_layer), rx_pilots.get_symbol(1, i_layer)));
479+
480+
// Use the first two DM-RS symbols to estimate the CFO.
481+
unsigned i_dmrs_0 = dmrs_mask.find_lowest(first_hop_symbol, last_hop_symbol);
482+
unsigned i_dmrs_1 = dmrs_mask.find_lowest(i_dmrs_0 + 1, dmrs_mask.size());
483+
484+
cf_t noisy_phase = srsvec::dot_prod(pilot_products, pilots_lse);
485+
486+
float cfo =
487+
std::arg(noisy_phase) / TWOPI / (i_dmrs_1 - i_dmrs_0 + cp_cum_duration[i_dmrs_1] - cp_cum_duration[i_dmrs_0]);
488+
489+
// Compensate the CFO in the first two DM-RS symbols and combine them.
490+
srsvec::sc_prod(pilots_lse, std::polar(1.0f, -TWOPI * (i_dmrs_0 + cp_cum_duration[i_dmrs_0]) * cfo), pilots_lse);
491+
srsvec::sc_prod(
492+
pilot_products, std::polar(1.0f, -TWOPI * (i_dmrs_1 + cp_cum_duration[i_dmrs_1]) * cfo), pilot_products);
493+
srsvec::add(pilots_lse, pilot_products, pilots_lse);
494+
495+
// If there are other DM-RS symbols in the hop, match them with the corresponding transmitted symbols, compensate the
496+
// CFO and combine with the previous DM-RS symbols.
497+
if (i_dmrs_1 < last_hop_symbol) {
498+
auto combine_pilots = [&, i_dmrs = 2](size_t i_symbol) mutable {
499+
srsvec::prod_conj(
500+
rx_pilots.get_symbol(i_dmrs, i_layer), pilots.get_symbol(hop_offset + i_dmrs, i_layer), pilot_products);
501+
srsvec::sc_prod(
502+
pilot_products, std::polar(1.0f, -TWOPI * (i_symbol + cp_cum_duration[i_symbol]) * cfo), pilot_products);
503+
srsvec::add(pilots_lse, pilot_products, pilots_lse);
504+
epre += std::real(srsvec::dot_prod(rx_pilots.get_symbol(i_dmrs, i_layer), rx_pilots.get_symbol(i_dmrs, i_layer)));
505+
i_dmrs++;
506+
};
507+
508+
dmrs_mask.for_each(i_dmrs_1 + 1, last_hop_symbol, combine_pilots);
509+
}
510+
511+
return {epre, cfo};
512+
}
513+
381514
static float estimate_noise(const dmrs_symbol_list& pilots,
382515
const dmrs_symbol_list& rx_pilots,
383516
span<const cf_t> estimates,

lib/phy/upper/signal_processors/port_channel_estimator_average_impl.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ class port_channel_estimator_average_impl : public port_channel_estimator
6565
unsigned hop,
6666
unsigned layer);
6767

68+
/// Computes the cumulative duration of all CPs (normalized with respect to the OFDM symbol time) for the given
69+
/// subcarrier spacing.
70+
void initialize_cp_cum_duration(cyclic_prefix cp, subcarrier_spacing scs);
71+
6872
/// Frequency domain smoothing strategy.
6973
port_channel_estimator_fd_smoothing_strategy fd_smoothing_strategy;
7074

@@ -83,6 +87,11 @@ class port_channel_estimator_average_impl : public port_channel_estimator
8387
/// Buffer of frequency response coefficients.
8488
std::array<cf_t, MAX_RB * NRE> freq_response;
8589

90+
/// Buffer of cumulative duration of CPs.
91+
std::array<float, MAX_NSYMB_PER_SLOT> cp_cum_help;
92+
/// View on the used part of the cumulative CP buffer \c cp_cum_help.
93+
span<float> cp_cum_duration;
94+
8695
/// Estimated RSRP value (single layer).
8796
float rsrp = 0;
8897

@@ -96,6 +105,9 @@ class port_channel_estimator_average_impl : public port_channel_estimator
96105

97106
/// Estimated time alignment in seconds.
98107
float time_alignment_s = 0;
108+
109+
/// Estimated CFO, normalized with respect to the subcarrier spacing.
110+
optional<float> cfo_normalized = nullopt;
99111
};
100112

101113
} // namespace srsran

tests/unittests/phy/upper/signal_processors/port_channel_estimator_test.cpp

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,9 @@ class ChannelEstFixture : public ::testing::TestWithParam<test_case_t>
7979

8080
std::shared_ptr<port_channel_estimator_factory> ChannelEstFixture::ch_est_factory = nullptr;
8181

82-
constexpr float tolerance = 5e-4;
83-
8482
bool are_estimates_ok(span<const resource_grid_reader_spy::expected_entry_t> expected, const channel_estimate& computed)
8583
{
84+
constexpr float tolerance = 5e-4;
8685
unsigned old_symbol = 15;
8786
span<const cf_t> computed_symbol;
8887

@@ -156,11 +155,19 @@ TEST_P(ChannelEstFixture, test)
156155
double tolerance_ta_us = 1e3 / (4096 * scs_to_khz(test_params.cfg.scs));
157156

158157
ASSERT_TRUE(are_estimates_ok(expected_estimates, estimates));
159-
ASSERT_NEAR(estimates.get_rsrp(0, 0), test_params.rsrp, tolerance);
160-
ASSERT_NEAR(estimates.get_epre(0, 0), test_params.epre, tolerance);
161-
ASSERT_NEAR(estimates.get_noise_variance(0, 0), test_params.noise_var_est, tolerance);
162-
ASSERT_NEAR(estimates.get_snr_dB(0, 0), test_params.snr_est, tolerance);
158+
ASSERT_NEAR(estimates.get_rsrp(0, 0), test_params.rsrp, 5e-4);
159+
ASSERT_NEAR(estimates.get_epre(0, 0), test_params.epre, 5e-4);
160+
ASSERT_NEAR(estimates.get_noise_variance(0, 0), test_params.noise_var_est, 0.004);
161+
ASSERT_NEAR(estimates.get_snr_dB(0, 0), test_params.snr_est, 0.02 * std::abs(test_params.snr_est));
163162
ASSERT_NEAR(estimates.get_time_alignment(0, 0).to_seconds() * 1e6, test_params.ta_us, tolerance_ta_us);
163+
if (test_params.cfo_est_Hz.has_value()) {
164+
ASSERT_TRUE(estimates.get_cfo_Hz(0, 0).has_value()) << "CFO estimation was expected, none obtained.";
165+
ASSERT_NEAR(estimates.get_cfo_Hz(0, 0).value(),
166+
test_params.cfo_est_Hz.value(),
167+
0.05 * std::abs(test_params.cfo_est_Hz.value()));
168+
} else {
169+
ASSERT_FALSE(estimates.get_cfo_Hz(0, 0).has_value()) << "No CFO estimation was expected.";
170+
}
164171
}
165172

166173
INSTANTIATE_TEST_SUITE_P(ChannelEstSuite, ChannelEstFixture, ::testing::ValuesIn(port_channel_estimator_test_data));

0 commit comments

Comments
 (0)