From 3d162344347eb98c96beeacf968c39187808f99a Mon Sep 17 00:00:00 2001 From: joebre Date: Thu, 1 Oct 2026 07:49:24 +0200 Subject: [PATCH 1/4] Fix PCPS acquisition sample-count truncation when samples_per_code is non-integer d_samples_to_consume (pcps_acquisition_adapter.cc) is floor(sampled_ms * samples_per_ms), truncating to an integer. When the configured internal_fs_sps does not divide evenly into a dwell (any decimation ratio leaving a fractional number of samples per dwell), every dwell is silently shorter than the true dwell length by a fixed residual, and that residual compounds linearly across non-coherent dwells: the true code epoch drifts within the accumulation grid instead of landing on the same index every time, and the sample stamp handed to tracking drifts out of sync with it. Tracking then reconstructs the wrong code phase at pull-in. Only shows up for short, high-chip-rate codes (Galileo E5a, GPS L5) accumulated over several dwells -- low chip rate codes (GPS L1, Galileo E1) absorb the same absolute drift as a much smaller fraction of a chip. Fix: keep each dwell's FFT/correlation window at the existing fixed d_samples_to_consume size (so none of the buffer/FFT-plan sizing set up at construction needs to change), but let the actual stream consumption between dwells track the true dwell boundary with a standard DDA/Bresenham accumulator -- skip one extra sample (discarded, never buffered) whenever the accumulated residual reaches a full sample. This keeps every dwell's window within half a sample of the true boundary permanently, so the drift this fix is about never accumulates in the first place. The residual itself is computed once, in the constructor, directly from the integer resampled_fs (not through the lossy intermediate float samples_per_ms), and is always in [0, 1) by construction, so there is no signed/unsigned wraparound risk. This lives entirely in the sample-consumption state machine in general_work(), upstream of doppler_grid_cpu()/doppler_grid_cuda()/update_synchro(), so CUDA builds and bit_transition_flag mode need no special-casing -- they share the same corrected consumption automatically. It is also correct for coherent_integration_time_ms spanning more than one code period, since the residual is derived from the same sampled_ms used for d_samples_to_consume, not from a single code period. A no-op whenever samples_per_code is already an integer (any internal_fs_sps dividing evenly into one dwell): the residual is then exactly 0, so the skip counter never fires. Verified against a real EVK1029 dual-band capture (Galileo E1+E5a, GPS L1+L5): the untouched case (samples/dwell already integer) stayed at ~2 spurious loss-of-lock events per 90s; a case with 0.5 samples/dwell of truncation error went from 50+ loss-of-lock events per 4 minutes (tracking's own frequency-error-reduction diagnostic scan showing no discernible correlation peak at all) down to 1 event per ~41s -- the same order as the untouched case -- with sharp, correct correlation peaks and CN0 estimates jumping from ~20-30 dB-Hz (meaningless, noise-floor) to ~42-52 dB-Hz (genuine lock). --- .../gnuradio_blocks/pcps_acquisition.cc | 26 +++++++++++++++++++ .../gnuradio_blocks/pcps_acquisition.h | 6 +++++ 2 files changed, 32 insertions(+) diff --git a/src/algorithms/acquisition/gnuradio_blocks/pcps_acquisition.cc b/src/algorithms/acquisition/gnuradio_blocks/pcps_acquisition.cc index 628aafd80..7d4daa302 100644 --- a/src/algorithms/acquisition/gnuradio_blocks/pcps_acquisition.cc +++ b/src/algorithms/acquisition/gnuradio_blocks/pcps_acquisition.cc @@ -127,6 +127,7 @@ pcps_acquisition::pcps_acquisition(const Acq_Conf& conf_) d_samplesPerChip(conf_.samples_per_chip), d_doppler_step(conf_.doppler_step), d_samples_to_consume(conf_.sampled_ms * conf_.samples_per_ms * (conf_.bit_transition_flag ? 2.0 : 1.0)), + d_dwell_residual_samples(std::clamp(static_cast(conf_.sampled_ms) * (conf_.bit_transition_flag ? 2.0 : 1.0) * static_cast(conf_.resampled_fs) / 1000.0 - static_cast(d_samples_to_consume), 0.0, 1.0 - 1e-9)), d_fft_size(conf_.sampled_ms == conf_.ms_per_code ? d_samples_to_consume : d_samples_to_consume * 2), d_effective_fft_size(conf_.bit_transition_flag ? (d_fft_size / 2) : d_fft_size), d_magnitude_grid_stride(aligned_row_stride(d_effective_fft_size)), @@ -155,6 +156,8 @@ pcps_acquisition::pcps_acquisition(const Acq_Conf& conf_) d_num_reference_rows_active(d_full_grid_reference_needs_extra_row ? (d_num_doppler_bins > 1U ? 2U : 1U) : 0U), d_threshold_active(conf_.pfa > 0.0 ? compute_threshold(conf_.pfa, d_effective_fft_size, d_num_doppler_bins, conf_.bit_transition_flag ? 1 : conf_.max_dwells) : conf_.threshold), d_buffer_sample_count(0), + d_dwell_residual_accum(0.0), + d_pending_skip_samples(0), d_channel(0), d_resampler_latency_samples(conf_.resampler_latency_samples), d_sample_count(0), @@ -1103,10 +1106,23 @@ int pcps_acquisition::general_work(int noutput_items __attribute__((unused)), d_gnss_synchro->Acq_doppler_step = 0U; d_state = 1; d_buffer_sample_count = 0U; + d_dwell_residual_accum = 0.0; + d_pending_skip_samples = 0U; break; } case 1: { + if (d_pending_skip_samples > 0U) + { + // Discard (do not buffer) samples to keep consumption + // aligned with the true dwell boundary; see below. + const auto skip_now = std::min(d_pending_skip_samples, static_cast(ninput_items[0])); + d_sample_count += static_cast(skip_now); + consume_each(skip_now); + d_pending_skip_samples -= skip_now; + break; + } + const auto fit_in_buffer = (ninput_items[0] + d_buffer_sample_count) <= d_samples_to_consume; const uint32_t samples_to_copy = fit_in_buffer ? ninput_items[0] : d_samples_to_consume - d_buffer_sample_count; @@ -1128,6 +1144,16 @@ int pcps_acquisition::general_work(int noutput_items __attribute__((unused)), if (d_buffer_sample_count == d_samples_to_consume) // Buffer is full { d_state = 2; + // d_samples_to_consume samples are always short of a true + // dwell by d_dwell_residual_samples; once that drift + // reaches a full sample, skip one extra so it never + // accumulates across dwells (DDA/Bresenham). + d_dwell_residual_accum += d_dwell_residual_samples; + if (d_dwell_residual_accum >= 1.0) + { + d_pending_skip_samples += 1U; + d_dwell_residual_accum -= 1.0; + } } break; diff --git a/src/algorithms/acquisition/gnuradio_blocks/pcps_acquisition.h b/src/algorithms/acquisition/gnuradio_blocks/pcps_acquisition.h index 581e14ba4..19d92a9a1 100644 --- a/src/algorithms/acquisition/gnuradio_blocks/pcps_acquisition.h +++ b/src/algorithms/acquisition/gnuradio_blocks/pcps_acquisition.h @@ -261,6 +261,8 @@ private: const uint32_t d_samplesPerChip; const uint32_t d_doppler_step; const uint32_t d_samples_to_consume; + // Exact (true) dwell length minus d_samples_to_consume, in [0, 1) samples. + const double d_dwell_residual_samples; const uint32_t d_fft_size; const uint32_t d_effective_fft_size; const uint32_t d_magnitude_grid_stride; @@ -301,6 +303,10 @@ private: // Recompute when the candidate count changes to preserve the requested PFA. float d_threshold_active; uint32_t d_buffer_sample_count; + // DDA/Bresenham accumulator keeping stream consumption aligned with the + // true dwell boundary; see general_work() case 0/1. + double d_dwell_residual_accum; + uint32_t d_pending_skip_samples; uint32_t d_channel; uint32_t d_resampler_latency_samples; uint64_t d_sample_count; From 9bd56d4cfe953e2ea93a94dee7459fe57a908072 Mon Sep 17 00:00:00 2001 From: joebre Date: Thu, 1 Oct 2026 08:18:13 +0200 Subject: [PATCH 2/4] Add a unit test for pcps_acquisition's per-dwell sample-count fix Covers the exact configuration the fix is for: GNSS-SDR.internal_fs_sps not a multiple of 1 kHz (4000123 Hz), coherent_integration_time_ms (4) spanning more than one code period (GPS L1 C/A's own code period is 1 ms), and max_dwells (5) > 1. Asserts a successful acquisition and checks the reported Doppler/delay against the known synthetic signal's truth, with tolerances the dwell-boundary drift this fix addresses would clearly exceed. Registering it in acq_test (tests/CMakeLists.txt) surfaced that the target had apparently never actually linked successfully in this environment before: it links FirFilter/SignalGenerator/GenSignalSource but was missing input_filter_adapters/signal_generator_adapters/ signal_source_adapters, and separately hit a Linux-only undefined libboost_thread symbol inside the system's packaged libgnuradio-runtime.so, fixed with the standard --copy-dt-needed-entries linker workaround (guarded to Linux only). Neither is specific to this new test; fixing them was necessary to build and run the suite at all, and the rest of acq_test (20 tests total) now passes too. --- tests/CMakeLists.txt | 13 +- .../pcps_acquisition_fractional_dwell_test.cc | 252 ++++++++++++++++++ 2 files changed, 264 insertions(+), 1 deletion(-) create mode 100644 tests/unit-tests/signal-processing-blocks/acquisition/pcps_acquisition_fractional_dwell_test.cc diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c7ddd32d3..b1fbb5bb3 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1272,6 +1272,7 @@ if(NOT ENABLE_PACKAGING AND NOT ENABLE_FPGA) ${CMAKE_CURRENT_SOURCE_DIR}/single_test_main.cc ${CMAKE_CURRENT_SOURCE_DIR}/unit-tests/signal-processing-blocks/acquisition/gps_l1_ca_pcps_acquisition_test.cc ${CMAKE_CURRENT_SOURCE_DIR}/unit-tests/signal-processing-blocks/acquisition/pcps_acquisition_doppler_narrowing_test.cc + ${CMAKE_CURRENT_SOURCE_DIR}/unit-tests/signal-processing-blocks/acquisition/pcps_acquisition_fractional_dwell_test.cc ) if(USE_CMAKE_TARGET_SOURCES) add_executable(acq_test) @@ -1280,9 +1281,15 @@ if(NOT ENABLE_PACKAGING AND NOT ENABLE_FPGA) add_executable(acq_test ${ACQ_TEST_SOURCES}) endif() + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + # Some distros' packaged libgnuradio-runtime.so does not declare an + # explicit NEEDED entry for libboost_thread even though it uses its + # symbols; GNU ld's default --no-copy-dt-needed-entries then fails to + # resolve them at link time despite Boost::thread being linked below. + target_link_options(acq_test PRIVATE -Wl,--copy-dt-needed-entries) + endif() target_link_libraries(acq_test PRIVATE - Boost::thread Gnuradio::runtime Gnuradio::blocks Gnuradio::filter @@ -1292,12 +1299,16 @@ if(NOT ENABLE_PACKAGING AND NOT ENABLE_FPGA) Matio::matio Volkgnsssdr::volkgnsssdr signal_generator_gr_blocks + signal_generator_adapters signal_source_gr_blocks signal_source_libs + signal_source_adapters + input_filter_adapters acquisition_adapters algorithms_libs signal_processing_testing_lib core_receiver + Boost::thread ) if(ENABLE_GLOG_AND_GFLAGS) target_link_libraries(acq_test PRIVATE Gflags::gflags Glog::glog) diff --git a/tests/unit-tests/signal-processing-blocks/acquisition/pcps_acquisition_fractional_dwell_test.cc b/tests/unit-tests/signal-processing-blocks/acquisition/pcps_acquisition_fractional_dwell_test.cc new file mode 100644 index 000000000..71cac5df8 --- /dev/null +++ b/tests/unit-tests/signal-processing-blocks/acquisition/pcps_acquisition_fractional_dwell_test.cc @@ -0,0 +1,252 @@ +/*! + * \file pcps_acquisition_fractional_dwell_test.cc + * \brief Regression test for pcps_acquisition's per-dwell sample-count + * truncation fix: GNSS-SDR.internal_fs_sps not a multiple of 1 kHz, combined + * with coherent_integration_time_ms spanning more than one code period and + * max_dwells > 1, which is exactly the configuration the original bug + * (floor() in d_samples_to_consume, and the resulting dwell-boundary drift + * across non-coherent dwells) depended on. + * + * ----------------------------------------------------------------------------- + * + * GNSS-SDR is a Global Navigation Satellite System software-defined receiver. + * This file is part of GNSS-SDR. + * + * Copyright (C) 2010-2026 (see AUTHORS file for a list of contributors) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * ----------------------------------------------------------------------------- + */ + + +#include "GPS_L1_CA.h" +#include "concurrent_queue.h" +#include "fir_filter.h" +#include "gen_signal_source.h" +#include "gnss_block_interface.h" +#include "gnss_sdr_valve.h" +#include "gnss_synchro.h" +#include "in_memory_configuration.h" +#include "pass_through.h" +#include "pcps_acquisition_adapter.h" +#include "signal_generator.h" +#include "signal_generator_c.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#if USE_GLOG_AND_GFLAGS +#include +#else +#include +#endif + +#if HAS_GENERIC_LAMBDA +#else +#include +#endif + +#if PMT_USES_BOOST_ANY +namespace wht = boost; +#else +namespace wht = std; +#endif + +// ######## GNURADIO BLOCK MESSAGE RECEIVER ######### +// Same pattern as the other acquisition test files in this directory, +// duplicated (not shared) to keep this file self-contained. +class PcpsAcquisitionFractionalDwellTest_msg_rx; + +using PcpsAcquisitionFractionalDwellTest_msg_rx_sptr = gnss_shared_ptr; + +PcpsAcquisitionFractionalDwellTest_msg_rx_sptr PcpsAcquisitionFractionalDwellTest_msg_rx_make(Concurrent_Queue& queue); + +class PcpsAcquisitionFractionalDwellTest_msg_rx : public gr::block +{ +private: + friend PcpsAcquisitionFractionalDwellTest_msg_rx_sptr PcpsAcquisitionFractionalDwellTest_msg_rx_make(Concurrent_Queue& queue); + void msg_handler_channel_events(const pmt::pmt_t& msg); + explicit PcpsAcquisitionFractionalDwellTest_msg_rx(Concurrent_Queue& queue); + Concurrent_Queue& channel_internal_queue; + +public: + int rx_message{0}; +}; + + +PcpsAcquisitionFractionalDwellTest_msg_rx_sptr PcpsAcquisitionFractionalDwellTest_msg_rx_make(Concurrent_Queue& queue) +{ + return PcpsAcquisitionFractionalDwellTest_msg_rx_sptr(new PcpsAcquisitionFractionalDwellTest_msg_rx(queue)); +} + + +void PcpsAcquisitionFractionalDwellTest_msg_rx::msg_handler_channel_events(const pmt::pmt_t& msg) +{ + try + { + int64_t message = pmt::to_long(msg); + rx_message = message; + channel_internal_queue.push(rx_message); + } + catch (const wht::bad_any_cast& e) + { + LOG(WARNING) << "msg_handler_channel_events Bad any_cast: " << e.what(); + rx_message = 0; + } +} + + +PcpsAcquisitionFractionalDwellTest_msg_rx::PcpsAcquisitionFractionalDwellTest_msg_rx(Concurrent_Queue& queue) + : gr::block("PcpsAcquisitionFractionalDwellTest_msg_rx", gr::io_signature::make(0, 0, 0), gr::io_signature::make(0, 0, 0)), channel_internal_queue(queue) +{ + this->message_port_register_in(pmt::mp("events")); + this->set_msg_handler(pmt::mp("events"), +#if HAS_GENERIC_LAMBDA + [this](auto&& PH1) { msg_handler_channel_events(std::forward(PH1)); }); +#else +#if USE_BOOST_BIND_PLACEHOLDERS + boost::bind(&PcpsAcquisitionFractionalDwellTest_msg_rx::msg_handler_channel_events, this, boost::placeholders::_1)); +#else + boost::bind(&PcpsAcquisitionFractionalDwellTest_msg_rx::msg_handler_channel_events, this, _1)); +#endif +#endif +} + + +// ########################################################### + +class PcpsAcquisitionFractionalDwellTest : public ::testing::Test +{ +protected: + PcpsAcquisitionFractionalDwellTest() + { + gnss_synchro = Gnss_Synchro(); + } + + Concurrent_Queue channel_internal_queue; + std::shared_ptr> queue; + gr::top_block_sptr top_block; + std::shared_ptr config; + Gnss_Synchro gnss_synchro; + + // Deliberately NOT a multiple of 1 kHz: 4000123 / 1000 = 4000.123, so + // d_samples_to_consume's floor() truncates a real, non-zero fraction of a + // sample on every dwell -- exactly the condition the fix is for. + static constexpr unsigned int kFsIn = 4000123; + // GPS L1 C/A's own code period is 1 ms; 4 ms coherent integration means + // sampled_ms (4) != ms_per_code (1), exercising the K > 1 case the + // original per-dwell drift formula got wrong. + static constexpr unsigned int kCoherentIntegrationMs = 4; + static constexpr unsigned int kMaxDwells = 5; + static constexpr double kExpectedDopplerHz = 750.0; + static constexpr double kExpectedDelayChips = 600.0; + // 2 / (3 * coherent_integration_time_s), the same bound every other test + // in this directory uses. + static constexpr double kMaxDopplerErrorHz = 2.0 / (3.0 * kCoherentIntegrationMs * 1e-3); +}; + + +TEST_F(PcpsAcquisitionFractionalDwellTest, NonIntegerSamplesPerCodeMultiDwellAcquires) +{ + gnss_synchro.Channel_ID = 0; + gnss_synchro.System = 'G'; + std::string signal = "1C"; + signal.copy(gnss_synchro.Signal, 2, 0); + gnss_synchro.PRN = 10; + + config = std::make_shared(); + config->set_property("GNSS-SDR.internal_fs_sps", std::to_string(kFsIn)); + + config->set_property("SignalSource.fs_hz", std::to_string(kFsIn)); + config->set_property("SignalSource.item_type", "gr_complex"); + config->set_property("SignalSource.num_satellites", "1"); + config->set_property("SignalSource.system_0", "G"); + config->set_property("SignalSource.PRN_0", "10"); + config->set_property("SignalSource.CN0_dB_0", "44"); + config->set_property("SignalSource.doppler_Hz_0", std::to_string(kExpectedDopplerHz)); + config->set_property("SignalSource.delay_chips_0", std::to_string(kExpectedDelayChips)); + config->set_property("SignalSource.noise_flag", "false"); + config->set_property("SignalSource.data_flag", "false"); + config->set_property("SignalSource.BW_BB", "0.97"); + + config->set_property("InputFilter.implementation", "Fir_Filter"); + config->set_property("InputFilter.input_item_type", "gr_complex"); + config->set_property("InputFilter.output_item_type", "gr_complex"); + config->set_property("InputFilter.taps_item_type", "float"); + config->set_property("InputFilter.number_of_taps", "11"); + config->set_property("InputFilter.number_of_bands", "2"); + config->set_property("InputFilter.band1_begin", "0.0"); + config->set_property("InputFilter.band1_end", "0.97"); + config->set_property("InputFilter.band2_begin", "0.98"); + config->set_property("InputFilter.band2_end", "1.0"); + config->set_property("InputFilter.ampl1_begin", "1.0"); + config->set_property("InputFilter.ampl1_end", "1.0"); + config->set_property("InputFilter.ampl2_begin", "0.0"); + config->set_property("InputFilter.ampl2_end", "0.0"); + config->set_property("InputFilter.band1_error", "1.0"); + config->set_property("InputFilter.band2_error", "1.0"); + config->set_property("InputFilter.filter_type", "bandpass"); + config->set_property("InputFilter.grid_density", "16"); + + config->set_property("Acquisition_1C.implementation", "GPS_L1_CA_PCPS_Acquisition"); + config->set_property("Acquisition_1C.item_type", "gr_complex"); + config->set_property("Acquisition_1C.coherent_integration_time_ms", std::to_string(kCoherentIntegrationMs)); + config->set_property("Acquisition_1C.max_dwells", std::to_string(kMaxDwells)); + config->set_property("Acquisition_1C.pfa", "0.001"); + config->set_property("Acquisition_1C.doppler_max", "5000"); + config->set_property("Acquisition_1C.doppler_step", "50"); + config->set_property("Acquisition_1C.bit_transition_flag", "false"); + config->set_property("Acquisition_1C.dump", "false"); + + queue = std::make_shared>(); + top_block = gr::make_top_block("Fractional dwell acquisition test"); + auto acquisition = std::make_shared(config.get(), "Acquisition_1C", "GPS_L1_CA_PCPS_Acquisition", 1, 0, GPS_1C); + auto msg_rx = PcpsAcquisitionFractionalDwellTest_msg_rx_make(channel_internal_queue); + + acquisition->set_channel(1); + acquisition->set_gnss_synchro(&gnss_synchro); + acquisition->connect(top_block); + top_block->msg_connect(acquisition->get_right_block(), pmt::mp("events"), msg_rx, pmt::mp("events")); + + std::shared_ptr signal_generator = std::make_shared(config.get(), "SignalSource", 0, 1, queue.get()); + std::shared_ptr filter = std::make_shared(config.get(), "InputFilter", 1, 1); + std::shared_ptr signal_source = std::make_shared(signal_generator, filter, "SignalSource", queue.get()); + signal_source->connect(top_block); + top_block->connect(signal_source->get_right_block(), 0, acquisition->get_left_block(), 0); + + acquisition->set_local_code(); + acquisition->reset(); + + int message = 0; + // SignalGenerator loops its generated vector indefinitely for a GPS-only + // source, so top_block->run() never returns on its own; stop it as soon + // as the acquisition message arrives, same as every other test in this + // directory does via its own wait_message()/process_message() pair. + std::thread wait_thread([&]() { + channel_internal_queue.wait_and_pop(message); + top_block->stop(); + }); + top_block->run(); + wait_thread.join(); + + ASSERT_EQ(1, message) << "Acquisition failure with a non-1kHz-multiple sampling rate, " + << "coherent_integration_time_ms (" << kCoherentIntegrationMs + << ") > ms_per_code, and max_dwells (" << kMaxDwells << ") > 1."; + + const double doppler_error_hz = std::abs(kExpectedDopplerHz - gnss_synchro.Acq_doppler_hz); + EXPECT_LE(doppler_error_hz, kMaxDopplerErrorHz) + << "Doppler error " << doppler_error_hz << " Hz exceeds " << kMaxDopplerErrorHz + << " Hz -- consistent with the dwell-boundary drifting across the " << kMaxDwells << " non-coherent dwells."; + + // Loose delay sanity check (generous +/-3 chip tolerance, no FIR-group-delay + // correction): the drift this test guards against would put the reported + // delay far outside this window, not just a fraction of a chip off. + const double samples_per_chip = static_cast(kFsIn) / GPS_L1_CA_CODE_RATE_CPS; + const double delay_error_chips = std::abs(kExpectedDelayChips - gnss_synchro.Acq_delay_samples / samples_per_chip); + EXPECT_LE(delay_error_chips, 3.0) << "Delay error " << delay_error_chips << " chips indicates the dwell-boundary tracking drifted."; +} From 3c256e991bd1b247ce95b65a3aed6d855dd44b6a Mon Sep 17 00:00:00 2001 From: Carles Fernandez Date: Thu, 1 Oct 2026 10:58:13 +0200 Subject: [PATCH 3/4] Compute PCPS dwell length with integer arithmetic Replace std::clamp with std::min/std::max for C++11/14 compatibility --- .../adapters/pcps_acquisition_adapter.cc | 2 +- .../gnuradio_blocks/pcps_acquisition.cc | 4 ++-- src/algorithms/acquisition/libs/acq_conf.cc | 18 ++++++++++++++++++ src/algorithms/acquisition/libs/acq_conf.h | 15 +++++++++++++++ tests/CMakeLists.txt | 9 ++------- .../pcps_acquisition_fractional_dwell_test.cc | 12 ++++++++++++ 6 files changed, 50 insertions(+), 10 deletions(-) diff --git a/src/algorithms/acquisition/adapters/pcps_acquisition_adapter.cc b/src/algorithms/acquisition/adapters/pcps_acquisition_adapter.cc index f10794cc0..d2a55c024 100644 --- a/src/algorithms/acquisition/adapters/pcps_acquisition_adapter.cc +++ b/src/algorithms/acquisition/adapters/pcps_acquisition_adapter.cc @@ -257,7 +257,7 @@ Acq_Conf get_acq_conf(const ConfigurationInterface* configuration, const std::st } #endif - acq_parameters.vector_length = std::floor(acq_parameters.sampled_ms * acq_parameters.samples_per_ms) * (acq_parameters.bit_transition_flag ? 2.0 : 1.0); + acq_parameters.vector_length = acq_parameters.GetSamplesPerDwell(); acq_parameters.code_length = static_cast(std::floor(static_cast(acq_parameters.resampled_fs) / (static_cast(sig_info.chip_rate) / sig_info.code_length_chips))); return acq_parameters; diff --git a/src/algorithms/acquisition/gnuradio_blocks/pcps_acquisition.cc b/src/algorithms/acquisition/gnuradio_blocks/pcps_acquisition.cc index 7d4daa302..09e7f65a8 100644 --- a/src/algorithms/acquisition/gnuradio_blocks/pcps_acquisition.cc +++ b/src/algorithms/acquisition/gnuradio_blocks/pcps_acquisition.cc @@ -126,8 +126,8 @@ pcps_acquisition::pcps_acquisition(const Acq_Conf& conf_) d_doppler_max(conf_.doppler_max), d_samplesPerChip(conf_.samples_per_chip), d_doppler_step(conf_.doppler_step), - d_samples_to_consume(conf_.sampled_ms * conf_.samples_per_ms * (conf_.bit_transition_flag ? 2.0 : 1.0)), - d_dwell_residual_samples(std::clamp(static_cast(conf_.sampled_ms) * (conf_.bit_transition_flag ? 2.0 : 1.0) * static_cast(conf_.resampled_fs) / 1000.0 - static_cast(d_samples_to_consume), 0.0, 1.0 - 1e-9)), + d_samples_to_consume(conf_.GetSamplesPerDwell()), + d_dwell_residual_samples(conf_.GetDwellResidualSamples()), d_fft_size(conf_.sampled_ms == conf_.ms_per_code ? d_samples_to_consume : d_samples_to_consume * 2), d_effective_fft_size(conf_.bit_transition_flag ? (d_fft_size / 2) : d_fft_size), d_magnitude_grid_stride(aligned_row_stride(d_effective_fft_size)), diff --git a/src/algorithms/acquisition/libs/acq_conf.cc b/src/algorithms/acquisition/libs/acq_conf.cc index acd56cb11..e9c149f30 100644 --- a/src/algorithms/acquisition/libs/acq_conf.cc +++ b/src/algorithms/acquisition/libs/acq_conf.cc @@ -133,6 +133,24 @@ void Acq_Conf::ConfigureAutomaticResampler(double opt_freq) } +uint64_t Acq_Conf::GetDwellSamplesTimes1000() const +{ + return static_cast(sampled_ms) * (bit_transition_flag ? 2ULL : 1ULL) * static_cast(resampled_fs); +} + + +uint32_t Acq_Conf::GetSamplesPerDwell() const +{ + return static_cast(GetDwellSamplesTimes1000() / 1000ULL); +} + + +double Acq_Conf::GetDwellResidualSamples() const +{ + return static_cast(GetDwellSamplesTimes1000() % 1000ULL) / 1000.0; +} + + void Acq_Conf::SetDerivedParams() { samples_per_ms = static_cast(resampled_fs) * 0.001F; diff --git a/src/algorithms/acquisition/libs/acq_conf.h b/src/algorithms/acquisition/libs/acq_conf.h index faf13f32e..902b1e3c3 100644 --- a/src/algorithms/acquisition/libs/acq_conf.h +++ b/src/algorithms/acquisition/libs/acq_conf.h @@ -37,6 +37,18 @@ public: void SetFromConfiguration(const ConfigurationInterface *configuration, const std::string &role, double chip_rate, double opt_freq); + /*! + * \brief Number of samples processed per dwell: floor(sampled_ms * resampled_fs / 1000), + * with the window doubled before flooring if bit_transition_flag is set. + * Computed with integer arithmetic, so it is exact for any resampled_fs. + */ + uint32_t GetSamplesPerDwell() const; + + /*! + * \brief Fraction of a sample per dwell dropped by GetSamplesPerDwell(), in [0, 1). + */ + double GetDwellResidualSamples() const; + /* PCPS Acquisition configuration */ std::string item_type{"gr_complex"}; std::string dump_filename; @@ -103,6 +115,9 @@ public: private: void SetDerivedParams(); + // True dwell length in samples, multiplied by 1000 (exact integer). + uint64_t GetDwellSamplesTimes1000() const; + void ConfigureAutomaticResampler(double opt_freq); }; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b1fbb5bb3..f7f17d7bc 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1281,13 +1281,6 @@ if(NOT ENABLE_PACKAGING AND NOT ENABLE_FPGA) add_executable(acq_test ${ACQ_TEST_SOURCES}) endif() - if(CMAKE_SYSTEM_NAME STREQUAL "Linux") - # Some distros' packaged libgnuradio-runtime.so does not declare an - # explicit NEEDED entry for libboost_thread even though it uses its - # symbols; GNU ld's default --no-copy-dt-needed-entries then fails to - # resolve them at link time despite Boost::thread being linked below. - target_link_options(acq_test PRIVATE -Wl,--copy-dt-needed-entries) - endif() target_link_libraries(acq_test PRIVATE Gnuradio::runtime @@ -1308,6 +1301,8 @@ if(NOT ENABLE_PACKAGING AND NOT ENABLE_FPGA) algorithms_libs signal_processing_testing_lib core_receiver + # Keep Boost::thread after the libraries that use it: with --as-needed, + # GNU ld drops a shared library listed before its first user. Boost::thread ) if(ENABLE_GLOG_AND_GFLAGS) diff --git a/tests/unit-tests/signal-processing-blocks/acquisition/pcps_acquisition_fractional_dwell_test.cc b/tests/unit-tests/signal-processing-blocks/acquisition/pcps_acquisition_fractional_dwell_test.cc index 71cac5df8..ebb151ebd 100644 --- a/tests/unit-tests/signal-processing-blocks/acquisition/pcps_acquisition_fractional_dwell_test.cc +++ b/tests/unit-tests/signal-processing-blocks/acquisition/pcps_acquisition_fractional_dwell_test.cc @@ -150,6 +150,18 @@ protected: static constexpr double kMaxDopplerErrorHz = 2.0 / (3.0 * kCoherentIntegrationMs * 1e-3); }; +#if __cplusplus < 201703L +// Before C++17, static constexpr data members are not implicitly inline, so +// the ones odr-used by gtest (bound to const T& in EXPECT_*/ASSERT_* and in +// their message streams) need an out-of-class definition to link. +constexpr unsigned int PcpsAcquisitionFractionalDwellTest::kFsIn; +constexpr unsigned int PcpsAcquisitionFractionalDwellTest::kCoherentIntegrationMs; +constexpr unsigned int PcpsAcquisitionFractionalDwellTest::kMaxDwells; +constexpr double PcpsAcquisitionFractionalDwellTest::kExpectedDopplerHz; +constexpr double PcpsAcquisitionFractionalDwellTest::kExpectedDelayChips; +constexpr double PcpsAcquisitionFractionalDwellTest::kMaxDopplerErrorHz; +#endif + TEST_F(PcpsAcquisitionFractionalDwellTest, NonIntegerSamplesPerCodeMultiDwellAcquires) { From b23a2af572daedcdd2584ab3102077e2d1930399 Mon Sep 17 00:00:00 2001 From: Carles Fernandez Date: Thu, 1 Oct 2026 11:57:15 +0200 Subject: [PATCH 4/4] Fix building --- src/algorithms/acquisition/gnuradio_blocks/CMakeLists.txt | 1 + tests/CMakeLists.txt | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/algorithms/acquisition/gnuradio_blocks/CMakeLists.txt b/src/algorithms/acquisition/gnuradio_blocks/CMakeLists.txt index aa049ae86..ec0d81ef2 100644 --- a/src/algorithms/acquisition/gnuradio_blocks/CMakeLists.txt +++ b/src/algorithms/acquisition/gnuradio_blocks/CMakeLists.txt @@ -66,6 +66,7 @@ target_link_libraries(acquisition_gr_blocks Volk::volk Volkgnsssdr::volkgnsssdr PRIVATE + Boost::thread Matio::matio ) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f7f17d7bc..578dc62d0 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1283,6 +1283,7 @@ if(NOT ENABLE_PACKAGING AND NOT ENABLE_FPGA) target_link_libraries(acq_test PRIVATE + Boost::thread Gnuradio::runtime Gnuradio::blocks Gnuradio::filter @@ -1301,9 +1302,6 @@ if(NOT ENABLE_PACKAGING AND NOT ENABLE_FPGA) algorithms_libs signal_processing_testing_lib core_receiver - # Keep Boost::thread after the libraries that use it: with --as-needed, - # GNU ld drops a shared library listed before its first user. - Boost::thread ) if(ENABLE_GLOG_AND_GFLAGS) target_link_libraries(acq_test PRIVATE Gflags::gflags Glog::glog)