log_time_to_fix_breakdown() and its six supporting state variables
were left-over instrumentation from developing the frequency-error-
reduction feature, not part of its documented behavior -- strip
before this is mergeable.
Follow-ups to the frequency-error-reduction merge (tracking state 5):
- Anchor Flag_PLL_180_deg_phase_locked to the sign of the last Prompt
symbol. The differential bit/secondary-code detector reconstructs bits
relative to an arbitrary first bit, so the previous corr_value sign rule
was independent of the actual carrier polarity.
- Do not feed the deliberate Doppler bin hops of the scan into the
high_dyn carrier/code phase-rate smoothers: clear the histories and
zero the rate steps while scanning.
- Start the pull_in_time_s and bit_synchronization_time_limit_s budgets
at the first sample after the scan instead of at acquisition, so long
scans no longer consume the FLL pull-in or the sync time limit. Scan
completion re-arms pull-in and resets the lock-fail counters. The
non-scan path keeps the acquisition time origin.
- Clamp f_error_accumulation to at least 1, both when parsed from the
configuration and in the block constructor, to avoid an out-of-bounds
write and a scan that never advances.
- Add unit tests for polarity, rate suppression, timers, and a full L2C
scan, and document the new options in the CHANGELOG.
Split out into its own PR (#1096) for independent review, since it's
an acquisition-only change unrelated to this PR's actual subject
(frequency-error-reduction tracking). This reverts commit
27832f4f53.
Follow-up to the galileo-navtype-fallback merge. The original fallback let
E5a/E5b/E6 observations use the other service's ephemeris, but the BGD index
for those signals is fixed by frequency, so an E5a range modeled with an
I/NAV clock would be off by BGD(E1,E5a) - BGD(E1,E5b). Only E1 selects its
BGD from the ephemeris clock model, so only E1 may fall back.
Drop the per-PRN locked set: I/NAV and F/NAV carry the same IODnav/toe,
so they expire together, and an E1 switch between services costs only BGD
quantization. select_galileo_ephemeris() is const again and simply prefers
the primary service while its ephemeris is fresh.
Also normalize a legacy Unknown provenance to the primary service on the
returned ephemeris, and allow E1 Reduced CED in F/NAV-primary receivers.
CI runs prettier with --prose-wrap always, which reflows Markdown
prose to the print-width regardless of existing line breaks; the
local default (preserve) had left the added bullet's original
wrapping untouched.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jb6oqf5UHnLP1yQoMdpb3S
EVK1029 is an Evaluation Kit for SAPHYRION SAGL space-grade ASICs: the
SY1009 RF Front-End and the SY1019 ADC/DSP. The kit includes a USB 3.0
device connected to a dual-band (E1/E5a) or triple-band (E1/E5a/E6) GNSS
front-end board, a Linux application for controlling/monitoring the
EVK1029 USB device and the SY10x9 ASICs and for saving the real-time
captured GNSS data stream to a binary file, and an EVK1029-enabled
version of GNSS-SDR.
This adds a signal source that reads the EVK1029 host application's raw
capture files directly: a continuous, header-less stream of OBA-encoded
4-bit samples (two per byte, 16 samples per little-endian 64-bit word),
with no periodic block header -- distinct enough from the generic
XML-metadata-driven ION_GSMS_Signal_Source path to warrant a dedicated
reader rather than a configuration of it.
Evk1029Source (the GNU Radio block) supports emitting multiple identical
output streams from a single read/file, so a dual-band capture (e.g. two
Freq_Xlating_Fir_Filter instances tuned to different IFs from the same
raw stream) stays sample-locked between bands: GNU Radio computes
noutput_items as the minimum available space across all declared output
ports, so backpressure from whichever downstream chain is slower stalls
production for every port together, instead of two independently-read
SignalSource instances that can silently drift apart over a long run.
The scheduler's output-multiple quantum is derived from the configured
sampling_frequency (targeting ~0.7 ms of data, rounded to the nearest
power of two) rather than hardcoded, since this block runs at the raw,
pre-decimation sample rate (up to ~180 Msps) where call-frequency
overhead measurably affects both this block and downstream readers.
Disabled by default, matching this project's convention for new
optional signal sources: build with -DENABLE_EVK1029=ON to enable it
(mirrors ENABLE_ION's wiring exactly -- CMake option, compile
definition on core_receiver, conditional registration in
gnss_block_factory.cc). Verified: clean build with the option off
(zero EVK1029 code compiled in) and with it on.
EVK1029 is being developed under an ESA contract and is expected to
become commercially available within 6-12 months.
Rtklib_Solver selects a single Galileo navigation service (I/NAV or F/NAV)
deterministically for the whole run based on which E5 signal is enabled.
For E1+E5a receivers this selects F/NAV, but F/NAV can take much longer to
decode than I/NAV (or never fully decode on a weak E5a signal), especially
when E1B is Doppler-assisting E5a acquisition via
GNSS-SDR.assist_dual_frequency_acq. Until now this meant such a satellite
was excluded from PVT entirely, even though a perfectly usable I/NAV
ephemeris was already available from E1B.
This fix is necessary for a cold start (no ephemeris/AGNSS assistance) to
work at all in an E1+E5a configuration with
GNSS-SDR.assist_dual_frequency_acq=true: without it, TTFF explodes because
the receiver sits waiting for F/NAV to decode on E5a before Galileo ever
contributes to the solution, even though a usable I/NAV ephemeris from
E1B is available much sooner.
select_galileo_ephemeris() now falls back to the other service per
satellite when the primary one isn't usable yet. Once a satellite
successfully uses the primary service it's locked onto it, so its
clock/orbit model never bounces between the two services once promoted.
The lock is released if the primary ephemeris later expires, so an
expired primary ephemeris never permanently excludes a satellite that
still has a usable ephemeris from the other service.
acquire_secondary()'s differential (delta-phase) demodulator only needs
inter-symbol phase drift to stay under +-90 deg -- a much looser
requirement than d_carrier_lock_test's coarse cos(2*phi) phase-lock
test (NBD/NBP over the Prompt correlator output). Gating symbol
accumulation on that phase-lock test was therefore blocking bit sync
in cases where the differential decoder would have worked fine, and
-- since the gate cleared the whole accumulation window on any brief
dip below threshold -- made bit sync sensitive to short-lived phase
instability that has nothing to do with whether consecutive symbols
can still be differentially decoded.
Removed the gate from both accumulation sites (secondary-code and
non-secondary preamble-correlation branches); accumulation now runs
unconditionally once pull-in has cleared, same as before this gate was
first added.
Added instrumentation (log_time_to_fix_breakdown(), gated on nothing
extra -- prints once per channel when bit/secondary-code sync
completes) reporting, per channel: total time from tracking start to
bit sync, how much of that was spent before first phase lock vs. after
it, how many accumulation-buffer resets happened, how many
acquire_secondary() attempts were made, and raw case-2 cycle counts.
This is what surfaced the fix above: several channels were shown to
complete bit sync entirely before ever reaching phase lock at all
(time_to_phase_lock_ms: never_reached), and channels that did reach
lock still showed hundreds of accumulation-buffer resets from brief
phase-lock flicker despite the carrier never truly losing lock by any
other measure.
Verified live against a real capture (single-band Galileo E1, 8
channels): 6/6 repeated runs after this change used all 8 configured
satellites in the first position fix, versus 6 of 8 before it -- some
channels' bit sync had not finished in time to contribute. Time to
first fix itself is unchanged (dominated by the separate, fixed
pull_in_time_s pull-in timer, unrelated to this gate), so the benefit
here is completeness/robustness of the first fix, not raw speed.
A prior attempt at this same gate removal was reverted for showing
loss-of-lock regressions in earlier testing, before this
diagnostic existed and before the carrier-aiding code-phase fix
in the previous commit. Flagging that history rather than claiming
certainty it's resolved -- this needs continued real-world testing,
not just the runs described above.
Signed-off-by: joebre <joel.brenner@saphyrion.ch>
Three fixes, developed and validated together, that the frequency-error
reduction scan's measured improvement (26/30 -> 4/30 trials with a
loss-of-lock event) depends on but that were missing from this PR:
- acquire_secondary(): replace the absolute-sign bit detector with a
differential (delta-phase) demodulator. Re{s[i] * conj(s[i-1])} < 0
means the bit flipped between symbols i-1 and i, so this only needs
inter-symbol phase stability, not absolute phase lock -- letting
bit/secondary-code sync start as soon as pull-in clears. The
d_carrier_lock_test >= d_carrier_lock_threshold gate on symbol
accumulation is kept (in both the secondary-code and non-secondary
preamble-search branches): A/B testing without it showed loss-of-lock
regressions and less repeatable runs, so accumulation is restarted
(buffer cleared) whenever lock drops mid-window instead.
- Doppler-aid the code rate used to bridge the acquisition-to-tracking
alignment gap, same formula run_dll() already applies every epoch.
Without this, the elapsed code phase over that gap (which varies
attempt to attempt with scheduling/threading jitter) is computed at
the bare nominal chip rate; the resulting uncompensated phase error
can reach a full chip or more at typical Doppler and gap values,
landing the very first correlation on an arbitrary, unpredictable
fractional-chip offset instead of the true code phase.
Signed-off-by: joebre <joel.brenner@saphyrion.ch>
Previously, the (possibly still noisy, partially non-coherently
accumulated) grid was checked against threshold after every single
dwell, and the first bin anywhere to cross it won immediately -- even
if a later dwell's fuller integration would have revealed a different,
genuinely stronger peak elsewhere in the same grid that never got the
chance to be compared.
Adds Acquisition_<Sig>.full_grid_search (default false, preserving the
existing early-exit behavior): when true, the accept/reject decision
is deferred until the final dwell, from the fully max_dwells-
accumulated grid, instead of exiting on the first dwell whose
still-partial accumulation happens to cross threshold.
Combined with the frequency-error reduction scan, this addresses a
recurring Galileo E1-B loss-of-lock failure mode: measured over 30
trials against a live capture, enabling full_grid_search alone still
left 26/30 trials with at least one loss-of-lock event (66 total
events); adding the frequency-error scan on top brought that down to
4/30 trials (5 total events).
Signed-off-by: joebre <joel.brenner@saphyrion.ch>
State 5's Doppler-bin scan had drifted into being interleaved with
state 2's normal tracking code (a per-epoch "is the scan still active"
branch inside run_dll()/run_dll_pll()) instead of staying a genuinely
separate state, making it harder to reason about and review in
isolation. Restores the original architecture: case 1 (pull-in) enters
state 5 when f_error_step_num != 0, state 5 is fully passive (drives
only the carrier, never touches the DLL/PLL loop filters), and
transitions to state 2 once all bins are scanned.
Also folds in refinements made since the original state-5 design:
- Per-bin scan results (Doppler tested, accumulated power, selected
bin) are dumped to a CSV file (Tracking_<Sig>.f_error_dump_filename,
default ./f_error_dump.csv) for offline inspection of the scan's
frequency-response envelope.
- A CN0 estimate for the winning bin, from its own raw Prompt samples.
- The carrier loop filter is explicitly re-seeded with the scan's
winning Doppler once it completes -- otherwise the first closed-loop
epoch after the scan would compute its output from the loop filter's
stale, acquisition-anchored internal state and jump back toward the
raw acquisition Doppler before re-converging on its own, discarding
the scan's result.
Because state 5 never runs the DLL/PLL loops at all, code phase and
the carrier loop filter's internal state stay exactly as pull-in left
them for the scan's whole duration -- this is what keeps Galileo E1
(BOC(1,1), whose autocorrelation has real side lobes, unlike GPS C/A's
clean single-peak triangle) from occasionally settling the code loop
onto a side lobe mid-scan, which previously produced a scrambled,
non-sinc-shaped envelope in the per-bin dump.
Signed-off-by: joebre <joel.brenner@saphyrion.ch>
Drop the redundant d_f_error_power() member-initializer (readability-
redundant-member-init: std::vector already default-constructs to
empty), and split f_error_bin_multiplier()'s integer division from
its cast to double into two statements so the intent (integer
half-step count, then converted to a Doppler multiplier) is explicit
rather than triggering bugprone-integer-division/modernize-use-auto.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: joebre <joel.brenner@saphyrion.ch>
Covers Dll_Pll_Conf::SetFromConfiguration() parsing of f_error_step_num
(default disabled, odd values pass through, even values round up to
odd, zero stays disabled) and f_error_accumulation/f_error_doppler_step
pass-through, plus dll_pll_veml_tracking::f_error_bin_multiplier()'s
bin-index-to-Doppler-offset mapping, including that step_num=5
reproduces the original fixed 5-bin scan order.
f_error_bin_multiplier() is made public so it can be exercised
directly from the test.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: joebre <joel.brenner@saphyrion.ch>
After pull-in and before entering wide tracking (state 2), optionally run
a passive Doppler bin scan around the pull-in Doppler estimate: for each
of an odd number of symmetric frequency bins (center, then alternating
+step/-step outward), correlate for a configurable number of incoherent
accumulations with no DLL/PLL filter updates. At the end of the scan, the
carrier Doppler is set to whichever bin produced the highest accumulated
power before the tracking filters are engaged. This helps in cases where
the residual carrier frequency error left after acquisition/pull-in would
otherwise leave the narrow tracking loops outside their pull-in range.
New per-tracking-role configuration parameters (all optional):
Tracking_1C.f_error_step_num = 5 ; number of Doppler bins to
; test. Must be odd; if an
; even value is given, 1 is
; added. 0 (the default)
; disables the mechanism
; entirely.
Tracking_1C.f_error_accumulation = 20 ; incoherent accumulations
; per bin
Tracking_1C.f_error_doppler_step = 250 ; bin spacing, in Hz
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: joebre <joel.brenner@saphyrion.ch>
Consuming input samples only when the decimation counter reaches it's
limit prevents decimation from working correctly. It just changes one-by-one
output to batch output and attempts to decimate batches instead of
individual synchos.
Consume unconditionally in every outer loop iteration and change loop
nesting to make sure, that all channels will be decimated equally and no
batches will be formed.
Add some safty checks against read beyond the end of the buffer and empty
array submission to Gnss_Synchro_Udp_Sink.
Signed-off-by: Vladslav P <vladisslav2011@gmail.com>
While a channel is idle (not actively searching), general_work() only
drains its input via consume_each() to avoid stalling the upstream
block -- it produces no output either way. Without any batching hint,
GNU Radio's thread-per-block scheduler was waking this block's thread
for every small burst of new input: observed live at 5,000-22,000
calls/s per idle channel, handling only ~850-3,400 samples each. Total
throughput matched the configured sample rate exactly (no excess data
movement), confirming this was pure per-call scheduling overhead
(mutex lock, function call, thread wake), not real work -- measured as
a steady 17-23% CPU per idle acquisition thread that should have been
doing nothing.
Fixed with set_output_multiple(), forcing the scheduler to accumulate
more input per wakeup. The batch size is derived from the signal's own
Acq_Conf::samples_per_ms (one PRN code period at that signal's
decimated rate) rather than a fixed constant: the algorithm never does
anything meaningful below that granularity, and deriving it self-scales
across signals/configs at different sample rates instead of being
tuned for one particular setup. Verified live: call rate dropped ~10x,
idle-thread CPU dropped from a steady 17-23% to mostly 3-12%. Also
tested output_multiple values well beyond samples_per_ms (up to 8x):
no further CPU reduction, confirming this is the practical floor for
this fix, not an arbitrarily chosen number.
- Rename doppler_uncertanty -> doppler_uncertainty throughout (interface,
adapter, block, comments, log message).
- Fix the out-of-bounds write and mis-detection in narrowed-mode
activation: track it with an explicit d_doppler_search_narrowed flag,
set directly in set_doppler_uncertainty() and guarded on
d_num_doppler_bins > 1, instead of inferring it from
"d_num_doppler_bins_active < d_num_doppler_bins". That inferred check
broke two ways: for a 1-bin full grid (a valid, already-accepted
config), it still forced 2 active bins, writing past the grid/wipeoff
allocations sized to d_num_doppler_bins; for a 2-bin full grid, an
actually-narrowed request computes the same active count as the full
grid (2 < 2 is false), so it silently searched the full grid's own
bin layout instead of the assisted {center, center + doppler_max}
pair. Reproduced the out-of-bounds write directly (temporarily
reverting to the inferred check crashes with "corrupted double-linked
list" under the new NarrowingEnabledOneBinGrid test) and the
mis-detection (NarrowingEnabledTwoBinGrid).
- Keep the noise-reference bin out of candidate selection: both
max_to_input_power_statistic() and first_vs_second_peak_statistic()
now take a separate candidate_count, used only for the peak-search
loop bound; the CFAR path's own num_doppler_bins (used for the
"opposite bin" reference lookup) is unchanged. In narrowed mode,
candidate_count is 1 -- the noise-reference bin can no longer win and
be reported as the acquisition result, regardless of its own power
(verified against a live signal deliberately placed in the reference
bin instead of the known/candidate bin: reverting this fix, both the
CFAR and peak-ratio statistic mistakenly report the reference bin's
Doppler as if it were the assisted center).
- Recalibrate the acceptance threshold for narrowed acquisition: adds
d_threshold_narrowed, computed like d_threshold but with
num_doppler_bins = 1 (the true candidate count in narrowed mode)
instead of the full grid's bin count, since compute_threshold() folds
the candidate count into the false-alarm probability and reusing the
full-grid threshold in narrowed mode was effectively far stricter
than the requested PFA. get_threshold() returns it whenever
d_doppler_search_narrowed is true.
- Fix narrowed dump layout/metadata: ensure_dump_grid_allocated() now
sizes acq_grid to d_num_doppler_bins_active (not the full grid width),
matching what copy_magnitude_grid_to_dump_grid() actually writes each
cycle -- this also makes narrow<->full transitions between dumps
self-correcting. dump_results() writes accurate, mode-appropriate
doppler_max/doppler_step (the same {0, doppler_max} encoding
compute_statistics() uses internally when narrowed), plus new
doppler_center and doppler_narrowed fields so offline post-processing
can tell which encoding is in effect. Verified end to end against a
live dump (NarrowedDumpMetadata).
- Strengthen Acq_Conf::enable_doppler_narrowing's doc comment (default,
which implementations it applies to, its dependency on the caller
passing doppler_uncertainty == 0) and add a commented usage example
next to GNSS-SDR.assist_dual_frequency_acq in a dual-frequency
Galileo E1B+E5a example config.
- Add tests/.../pcps_acquisition_doppler_narrowing_test.cc: 8 focused
tests (narrowing enabled/disabled, 1-bin and 2-bin full-grid
configurations, CFAR and peak-ratio statistics, a full-narrow-full
transition, two-step acquisition with step-1 narrowing, and narrowed
dump metadata), built on the same real-capture/known-ground-truth
pattern as gps_l1_ca_pcps_acquisition_test.cc. Confirmed each targeted
regression test actually fails without its corresponding fix before
finalizing them.
Signed-off-by: joebre <joel.brenner@saphyrion.ch>