Doppler estimators and loop filters
A Doppler estimator closes the carrier and code loops. Its configuration (loop filter types and bandwidths) is a plain value, a subtype of AbstractDopplerEstimator; the per-satellite state it advances is created with init_estimator_state. Every estimator is stepped with the same call,
state, carrier_doppler, code_doppler = step_loop(estimator, state, record, words, landing_sample)where record is a LoopRecord, words is the replica frequency the record was produced under (a FixedNCOWord for a software correlator, the channel's NCOTimeline for a hardware one) and landing_sample is the sample the new command takes effect at, or NO_LANDING_SAMPLE for "at the end of each record".
A host steps every record of a satellite, of every signal it tracks. The state remembers the driver it was initialised with: the driver's records close the loops, and the scalar loops leave any other record (a passenger, such as the data component of a pilot + data pair) to the host, returning the state unchanged and the command in force.
ConventionalPLLAndDLL: a PLL and a carrier-aided DLL.ConventionalAssistedPLLAndDLL: the same with an FLL assisting the PLL until phase lock (see Carrier-loop staging), to pull in larger initial frequency errors. The default in Tracking.jl.NCOReferencedPLLAndDLL: stays stable when its correction takes effect only several records later, as with a hardware NCO. With a fixed word and no landing sample it is the FLL-assisted conventional loop at the same bandwidths.
The bandwidth rules (default_wide_carrier_loop_filter_bandwidth, effective_carrier_loop_filter_bandwidth, …) keep the loops stable for a given integration time; the integration-length rules (calc_num_code_blocks_to_integrate, …) say how long a signal may be integrated coherently once its bit or secondary code has been found.
The per-record fold
apply_record advances one signal component's SignalLoopState (its prompt filter, C/N₀ estimator and bit buffer) by one record, so every caller does it the same way.
TrackingLoops.SignalLoopState — Type
SignalLoopState(signal; num_prompts_for_cn0_estimation = 100, cn0_estimator,
post_corr_filter)The per-record state of one signal component on a satellite: bit buffer, C/N₀ estimator, post-correlation filter, the last filtered prompt (the FLL chains from it) and the last record's block count. Immutable, rebuilt per record by apply_record; the estimators own their buffers, so every component needs its own instance: one per signal of every satellite, driver and passengers alike.
It also keeps what the last record did, for a LoopRecord built from it: last_sync_change (a SyncChange), last_num_new_soft_bits (the soft bits it appended), last_previous_prompt (the prompt the FLL compares its prompt with: the one before, or 0 across a change of carrier phase polarity) and last_carrier_phase_polarity (its prompt's sign where known, else 0; see LoopRecord).
TrackingLoops.SignalLoopState — Method
SignalLoopState(state::SignalLoopState; bit_buffer, cn0_estimator, post_corr_filter,
last_filtered_prompt, last_num_code_blocks, last_sync_change,
last_num_new_soft_bits, last_previous_prompt,
last_carrier_phase_polarity)state with the fields passed replaced, every other one kept, so a host that swaps a part of the state (a C/N₀ estimator, say) need not list the rest.
TrackingLoops.SyncChange — Type
SyncChangeWhat one record did to a signal's bit clock (see SignalLoopState and LoopRecord):
SYNC_UNCHANGED: neither found nor lost the bit or secondary-code sync;SYNC_FOUND: found it;SYNC_LOST: lost it — a post-sync record that overshot the navigation-bit boundary, on which the bit buffer restarted its search.
TrackingLoops.apply_record — Function
apply_record(state::SignalLoopState, signal, prn, output, sampling_frequency,
noise_density, noise_density_ready,
driver_carrier_phase = get_carrier_phase_offset(signal);
overlay_wiped = has_bit_or_secondary_code_been_found(state))
-> (state, prompt, filtered_correlator, integrated_code_blocks, overshoot)fold_record on a SignalLoopState. Returns the new state, the prompt, the filtered correlator the discriminators read, the blocks the record covered and overshoot: whether a post-sync record carried the bit accumulator past the bit boundary, so the buffer dropped sync and restarted its search (SYNC_LOST; JuliaGNSS/Tracking.jl#238). The new state keeps the filtered prompt and what the record did, for a LoopRecord built from it. Nothing here logs; the caller reports overshoot (Tracking.jl warns once per satellite).
driver_carrier_phase is the carrier-phase offset (get_carrier_phase_offset) of the satellite's estimator-driver signal; the bit-buffer prompt is de-rotated against it. The default (the signal's own) suits the driver; a passenger component (the data half of a pilot/data pair) must be handed the driver's.
The pre-sync prompts of an is_dataless_with_secondary_code signal must be primary-only, so that the sync's polarity is the prompt's sign once the overlay is wiped. For any other signal the sync polarity only fixes the soft bits' sign, which the Costas loop leaves ambiguous anyway, so a fixed secondary chip in the pre-sync replica is fine.
overlay_wiped says whether the record's replica wiped the secondary code at the synced phase; it defaults to the buffer having synced before the record. Pass false for a record after a sync it was not correlated for yet (e.g. one found earlier in the same fold): for a secondary-coded signal its prompt is kept out of the coherent sum, though its blocks are credited, it moves the secondary-code anchor and its polarity is 0.
TrackingLoops.is_dataless_with_secondary_code — Method
is_dataless_with_secondary_code(signal) -> BoolWhether signal is dataless with a secondary code: GPS L1C-P and L5Q, Galileo E1C, E5a-Q, E5b-Q and E6-C, BeiDou B1C-P and B2a-Q. Synced and correlated with the overlay wiped, such a signal's prompt keeps the sign the sync read, so the four-quadrant carrier discriminators apply. Its pre-sync prompts must be primary-only: a host whose pre-sync replica carries a secondary chip uses this to pick the records to multiply by that chip.
TrackingLoops.reset_signal_state — Method
reset_signal_state(state::SignalLoopState) -> SignalLoopStateThe state a freshly armed channel starts from (no sync, no soft bits, empty C/N₀ estimator, no previous prompt), reusing the old vectors so a re-arm allocates nothing. The post-correlation filter is kept: it is configuration, not history; an adaptive filter must be reset by its owner.
A custom AbstractCN0Estimator with history needs a TrackingLoops._reset_cn0_estimator method; without one this throws rather than hand the new satellite the old one's C/N₀.
TrackingLoops.restart_bit_clock — Method
restart_bit_clock(state::SignalLoopState) -> SignalLoopStateThe state with a fresh, unsynchronised bit buffer (what a lost record costs), everything else kept.
Two- and four-quadrant discriminators
The carrier discriminators are two-quadrant (Costas) by default: they read a prompt and its negation alike, so a data bit or secondary-code chip flipping the prompt's sign does not disturb them, over ±1/4 cycle for the PLL and ±1/(4T) for the FLL. Where the replica wipes every sign modulation off the prompt (a dataless signal synced to its secondary code) the prompt's sign is known, and the estimators switch to the four-quadrant discriminators, over ±1/2 cycle and ±1/(2T), worth up to 6 dB of tracking threshold. The record's carrier_phase_polarity, the prompt's sign, says so; apply_record computes it into the SignalLoopState, and a LoopRecord built from that state carries it to the scalar and vector loops alike. A record whose polarity differs from the previous record's comes without a previous prompt, so the four-quadrant FLL never compares prompts across the switch.
GPS L1C-P and L5Q, Galileo E1C, E5a-Q, E5b-Q and E6-C, and BeiDou B1C-P and B2a-Q switch once their secondary code is synced; if the Costas loop slipped half a cycle between the sync and the switch, the four-quadrant PLL pulls the carrier phase over by half a cycle, after which the prompt agrees with the sign again. Data signals never switch, nor, for now, do the pilots without a secondary code (GPS L2 CL, Galileo E5a-QP), whose sync reads no sign. The NCO-referenced loop folds its predicted phase error into whichever range the record's PLL reads.
The sign rests on a contract with whoever correlates the records, a hardware correlator as much as Tracking.jl: until the sync, these signals' records (is_dataless_with_secondary_code) are primary-only. A replica carrying the same secondary chip on every block is fine once the host multiplies the records by that chip (Tracking.jl's carries chip 0 and multiplies by it). A negative chip left in makes the sync read the wrong sign, and on the switch the four-quadrant PLL pulls the carrier phase over by half a cycle.
Estimators
TrackingLoops.ConventionalPLLAndDLL — Type
Conventional PLL and DLL Doppler estimator. Configuration only; per-satellite state is a SatConventionalPLLAndDLL from init_estimator_state.
CA and CO are the carrier and code loop filter types. The bandwidths (the PLL's wide and narrow, the FLL path's and the DLL's; see CarrierLoopStage) default to nothing, auto: sized per satellite from its driver signal via default_wide_carrier_loop_filter_bandwidth, default_narrow_carrier_loop_filter_bandwidth, default_fll_assist_loop_filter_bandwidth and default_code_loop_filter_bandwidth. Each is capped against the record's integration time at filter time (see LoopBandwidths), so longer integration needs no re-tuning.
TrackingLoops.LoopRecord — Type
One completed record as the loop-filter step sees it: the signal, the filtered (antenna-combined, normalised) correlator, the previous filtered prompt the FLL compares with, the record's span and block count, the band's sampling frequency, and fold_end, the end sample of the fold's last record, against which a landing sample is measured.
integrated_code_blocks must be at least 1: the carrier-loop staging sizes its phase-lock indicator's averages in records of that length and divides by it. A record built from a SignalLoopState always is (apply_record counts at least one block); a host building records otherwise must ensure it.
The four-quadrant FLL reads a prompt sign flip as half a cycle of rotation, so a record whose carrier_phase_polarity differs from the previous record's comes with previous_prompt = 0, giving no FLL reading, as for the first record. The constructor that takes a SignalLoopState applies this; a host building records otherwise passes 0 itself.
Fields for an estimator with per-satellite state of its own (VectorPLLAndDLL):
prn: the satellite (0if unknown);code_phase: the replica's code phase (chips) atsample_index, from theCorrelatorOutput(NaNif not reported).
For such an estimator sample_index / sampling_frequency must also be the time since an origin shared by every satellite of the band; a host whose correlator restarts its sample count passes the offset as sample_offset (added to sample_index and fold_end).
The rest summarises the signal's SignalLoopState after apply_record folded this record, for an estimator that decodes or weights by C/N₀ (the vector loop; the scalar loops ignore it):
bit_synced: whether the bit clock holds bit or secondary-code sync;sync_change: whether this record found or lost it (SyncChange);blocks_into_symbol: code blocks integrated into the current data symbol (0before sync);new_soft_bits: exactly the soft bits this record appended, a view of the bit buffer's vector valid for the call, so it does not matter when the host drains the buffer;cn0_estimator: the signal's C/N₀ estimator, forestimate_cn0;bit_polarity: the sign the bit clock's sync found its soft bits at (-1if it negates the accumulated prompts' real part,+1if it keeps it,0before sync).
carrier_phase_polarity is the prompt's sign where the replica wiped every sign modulation off it (a dataless signal synced to its secondary code), else 0. Nonzero, the PLL and the FLL read the record four-quadrant (pll_disc(...; polarity), fll_disc(...; polarity)); 0, two-quadrant.
The constructor taking a SignalLoopState fills all of this in. The others leave the bit clock unsynchronised, add no soft bits, carry a NoCN0Estimator and take carrier_phase_polarity as a keyword (0 by default): a host with its own per-signal state passes the one fold_record returned for the record.
TrackingLoops.LoopRecord — Method
LoopRecord(signal, filtered_correlator, output::CorrelatorOutput,
state::SignalLoopState, sampling_frequency;
fold_end = output.sample_index, prn = 0, sample_offset = 0)The record of output with its block count, previous prompt, bit-clock summary, C/N₀ estimator and carrier phase polarity taken from state, the SignalLoopState that apply_record just folded output into (see LoopRecord for the fields).
TrackingLoops.NCOReferencedPLLAndDLL — Type
NCOReferencedPLLAndDLL(; wide_carrier_loop_filter_bandwidth = nothing,
narrow_carrier_loop_filter_bandwidth = nothing,
fll_assist_loop_filter_bandwidth = nothing,
code_loop_filter_bandwidth = nothing,
predict_landing = true)FLL-assisted PLL and DLL Doppler estimator for a replica whose NCO words are applied with a known delay; the hardware-correlator receiver's default.
It is the ConventionalAssistedPLLAndDLL (same filters, gains, CarrierLoopStage staging and bandwidth defaults) with its internals referenced to the device NCO. The one different default is the wide PLL bandwidth: the narrow one (default_narrow_carrier_loop_filter_bandwidth), as a wider loop tolerates less command delay.
- Every record is attributed to the word that ran under it. Both discriminators are measured against the applied replica, so
applied word + FLL discriminatoris an absolute Doppler measurement; the DLL is normalised with the applied code word. The conventional loop does this too. - The correction is sized for the moment it lands. The filter is fed the discriminators predicted at the new command's landing sample: the phase error (cycles) advanced by
∫ (f̂ − w(τ)) dτover the words already scheduled, and the frequency measurement re-based onto the word running there.
With zero delay step 2 is the identity and the estimator is the conventional loop at the same bandwidths, inheriting its noise performance. predict_landing = false drops step 2, making it arithmetically the ConventionalAssistedPLLAndDLL: the documented negative control, which fails like it at a few epochs of delay.
TrackingLoops.SatConventionalPLLAndDLL — Type
Per-satellite state of a ConventionalPLLAndDLL: initial Dopplers, loop filters, their LoopBandwidths, the key of the driver signal whose records close the loops (0: every record's; see step_loop), and the CarrierLoopStaging.
TrackingLoops.SatNCOReferencedPLLAndDLL — Type
SatNCOReferencedPLLAndDLLPer-satellite state of an NCOReferencedPLLAndDLL: handover Dopplers (the filters output offsets from them), both filters, their bandwidths, the centre sample of the last record (NaN before the first; the FLL measures between two prompts' centres, so its replica word is averaged over that span), and the driver key and staging as in SatConventionalPLLAndDLL.
TrackingLoops.ConventionalAssistedPLLAndDLL — Method
ConventionalAssistedPLLAndDLL(; ...)
ConventionalAssistedPLLAndDLL(
;
wide_carrier_loop_filter_bandwidth,
narrow_carrier_loop_filter_bandwidth,
fll_assist_loop_filter_bandwidth,
code_loop_filter_bandwidth
)
A ConventionalPLLAndDLL with the FLL-assisted carrier filter ThirdOrderAssistedBilinearLF (see CarrierLoopStage). Keywords as there.
TrackingLoops.carrier_loop_stage — Method
carrier_loop_stage(state) -> CarrierLoopStageThe CarrierLoopStage of a scalar estimator's per-satellite state, or of a VectorPLLAndDLL's: VECTOR_LOOP while the satellite is in the vector loop, the inner loop's stage out of it.
TrackingLoops.carrier_phase_correction — Method
carrier_phase_correction(state, words, sample, sampling_frequency) -> Float64The carrier phase (cycles) the loop estimates the incoming carrier to lead the replica by at sample (of the records' grid at sampling_frequency), with words the words the replica ran on up to there, an NCOTimeline or a FixedNCOWord. The replica's carrier phase plus this is the loop's estimate of the incoming carrier phase, the carrier-phase measurement a host reports (RINEX L).
A scalar loop steers its replica onto the incoming carrier, so the replica's phase is its estimate and this is 0.0; so is a VectorPLLAndDLL's out of the vector loop. In it the navigation filter sets the carrier NCO and the residual PLL follows the carrier the NCO leaves without steering it, so the replica's phase alone drifts off the carrier, by whole cycles over time: this is the residual PLL's prediction at sample, from its latest prompt on, counting every cycle, never folded (0.0 before its first prompt). Meant for a sample at or after the end of the driver's latest record, as a host's replica phase after its last record is.
Like a scalar PLL's replica phase, it is ambiguous by half a cycle under the Costas discriminator, and phase_locked says whether it can be trusted. It steps where the loop does not follow the carrier continuously, which a RINEX host flags in the loss-of-lock indicator:
- by half a cycle when the record's
carrier_phase_polarityturns four-quadrant (a synced pilot) and the residual PLL finds itself half a cycle off the polarity's frame; - on a release from the vector loop, when
carrier_loop_stageleavesVECTOR_LOOP: the correction drops to zero while the scalar loop pulls its replica in afresh from where the NCO left it, so the phase steps by the residual and may slip.
A satellite added to the vector loop keeps its phase: the residual PLL starts at zero on the phase-locked scalar loop's replica.
TrackingLoops.estimator_state_type — Method
The estimator-state type a Doppler estimator produces (for slot typing).
TrackingLoops.init_estimator_state — Method
init_estimator_state(estimator, driver_signal, carrier_doppler, code_doppler)Per-satellite estimator state for a satellite driven by driver_signal, starting at the given Dopplers; auto bandwidths are resolved here from driver_signal. The state records the driver, so step_loop tells its records from the satellite's other signals', and starts the CarrierLoopStage staging. Must be pure: Tracking.jl also calls it for template states and re-seeding.
TrackingLoops.navigation_cycle — Method
navigation_cycle(estimator) -> Union{Int,Nothing}Number of navigation cycles run, or nothing without them. Changes exactly when navigation_solution and navigation_status do, so polling it reads each solution once.
TrackingLoops.navigation_epoch — Method
navigation_epoch(estimator) -> Union{typeof(1.0s),Nothing}The epoch of the latest navigation solution as sample_index / sampling_frequency; nothing before the first cycle or without cycles.
TrackingLoops.navigation_solution — Method
navigation_solution(estimator) -> Union{PVTSolution,Nothing}The latest navigation solution (position, velocity, time, clock bias and drift, DOP, the satellites used with their residuals, inter-system and inter-frequency biases), or nothing for the scalar loops. For VectorPLLAndDLL see its method.
TrackingLoops.navigation_status — Method
navigation_status(estimator) -> Union{NavigationStatus,Nothing}What the latest navigation cycle did (NavigationStatus), or nothing for an estimator without one.
TrackingLoops.phase_lock_indicator — Method
phase_lock_indicator(state) -> Float64The latest phase-lock indicator of a scalar estimator's per-satellite state, or of a VectorPLLAndDLL's (in the vector loop its residual PLL's, on the prompts it rotated; out of it the inner loop's), which the carrier-loop staging reads (see CarrierLoopStage): ⟨I² − Q²⟩ / A² of the driver's prompts, an estimate of cos 2φ for the carrier phase error φ (1 in lock, 0 for a spinning phase), averaged over 0.1 s or 25 records, whichever is longer. The signal power A² = √(2 M₂² − M₄), from the prompts' moments Mₖ = ⟨|P|ᵏ⟩, is free of noise, so unlike ⟨I² − Q²⟩ / ⟨I² + Q²⟩ a locked loop's reading gets noisier at low C/N₀, not lower. NaN until the averages span that time.
TrackingLoops.phase_locked — Method
phase_locked(state) -> BoolWhether the carrier loop of a scalar estimator's per-satellite state, or of a VectorPLLAndDLL's, is phase-locked, as the loop itself decides it: a scalar loop while its phase_lock_indicator reads at least 0.5, the staging's threshold (false while NaN). In the vector loop it is the residual PLL's lock, which also asks that the C/N₀ the rotated prompts' moments read be at most 6 dB below the estimator's lock_cn0_threshold, as on bare noise the indicator alone reads lock now and then. Out of the vector loop, the inner loop's.
TrackingLoops.reset_estimator_state — Method
reset_estimator_state(estimator, state, carrier_doppler, code_doppler)Re-seed state at the given Dopplers with zeroed loop filters, keeping the bandwidths and the carrier loop's staging (see CarrierLoopStage): a reset is a handoff, not a re-acquisition, so after a change of integration length, say, the loop holds its stage, the phase-lock indicator reads on, and the bandwidths are capped for the new length.
TrackingLoops.satellite_report — Method
satellite_report(estimator, signal, prn) -> Union{SatelliteReport,Nothing}A SatelliteReport of satellite prn of signal, or nothing for the scalar loops or a satellite never seen.
TrackingLoops.step_loop — Method
step_loop(estimator::ConventionalPLLAndDLL, state, record::LoopRecord, words,
landing_sample)
-> (state, carrier_doppler, code_doppler)One record through the conventional loop at the bandwidths of its CarrierLoopStage, capped against the record's integration time; the DLL is normalised with the code word the record ran on, and the Dopplers are aided (aid_dopplers). landing_sample is ignored: the command is assumed to act before the next record.
A host hands every record of a satellite to step_loop, the driver's and its passengers' alike; only the driver's step the loop. A passenger record leaves the state as it is and returns the command in force at the landing sample (the record's end under NO_LANDING_SAMPLE), so applying it changes nothing.
TrackingLoops.step_loop — Method
step_loop(estimator::NCOReferencedPLLAndDLL, state, record::LoopRecord, words,
landing_sample)
-> (state, carrier_doppler, code_doppler)One record through the NCO-referenced loop. words are the replica words the record ran on; landing_sample is where this fold's command lands (NO_LANDING_SAMPLE: at the record's end). See NCOReferencedPLLAndDLL. Passenger records are handled as by the conventional loop.
Carrier-loop staging
The scalar loops stage their carrier loop on phase lock: FLL-assisted at the wide PLL bandwidth, then a pure PLL at the wide bandwidth, then narrow, each stage moving on once the phase-lock indicator (phase_lock_indicator) has read lock (phase_locked: at least 0.5, an RMS phase error of about 30°) long enough. The stages and when they advance are given under CarrierLoopStage; in the vector loop, see The carrier in the vector loop.
A wider loop tolerates less command delay: at 50 Hz the conventional loop holds through three records of delay. The NCOReferencedPLLAndDLL, meant for hardware correlators whose commands land records later, therefore pulls in at the narrow 18 Hz by default.
TrackingLoops.CarrierLoopStage — Type
CarrierLoopStageThe stage of a satellite's carrier loop in the scalar estimators' per-satellite state (see carrier_loop_stage):
FLL_ASSISTED_PLL: FLL-assisted PLL at the wide bandwidth (default_wide_carrier_loop_filter_bandwidth), its FLL path atdefault_fll_assist_loop_filter_bandwidth, until the phase-lock indicator (phase_lock_indicator) has read lock (phase_locked) without a break for one time constant of its averages: phase lock implies a converged Doppler;WIDE_PLL: PLL alone at the wide bandwidth, the FLL discriminator no longer formed, until lock has held unbroken for another four time constants;NARROW_PLL: PLL alone at the narrow bandwidth (default_narrow_carrier_loop_filter_bandwidth);VECTOR_LOOP: no loop filter on the carrier NCO, which the navigation filter sets while the satellite is in the vector loop; its residual PLL follows the carrier (seeVectorPLLAndDLL'sresidual_pll_loop_filter_bandwidth). The scalar staging never reaches it; only a satellite in the vector loop reports it.
A filter without an FLL path starts at WIDE_PLL. Dropping the FLL feeds the assisted filter a zero FLL input, which is exactly the third-order PLL (same state, same coefficients), so the switch is free (Kaplan & Hegarty §5.5). Dropping the FLL and narrowing are separate steps: narrowing while the loop still settles from the FLL's last correction risks the lock.
Each driver record's prompt advances the indicator, and the stage it reaches applies from the next record. Stages only advance: a loop that loses lock keeps the narrow PLL. init_estimator_state starts them over, as does a satellite's release from the vector loop; reset_estimator_state keeps them (a handoff to a new integration length, say, keeps the lock). In the vector loop the staging stands still.
Loop-filter bandwidths
The PLL pulls in at a wide 50 Hz and narrows to 18 Hz once phase lock has held; the FLL path of the FLL-assisted filter runs at 5 Hz while it is in use, and the code loop at 1 Hz, all for every signal. All are one-sided noise bandwidths BL (Kaplan & Hegarty), so they plug into the usual jitter and dynamic-stress formulas: the carrier filter is fed the phase error in cycles and the FLL error in Hz. 18 Hz is the third-order PLL bandwidth of the literature (Kaplan & Hegarty Table 5.6, Pany Table 3.3; Borre et al. quote about 20 Hz), none of which scales it with the primary code period: thermal jitter and dynamic stress, which set the bandwidth, do not depend on it.
Stability cap
Stability does depend on the loop update interval Δt, so at filter time every bandwidth, default or explicit, is capped against the record's integration time (effective_carrier_loop_filter_bandwidth, effective_fll_assist_loop_filter_bandwidth, effective_code_loop_filter_bandwidth). The cap only ever narrows a loop.
BL_pll = min(BL_pll_configured, 0.09 / Δt) # wide and narrow alike
BL_fll = min(BL_fll_configured, 0.02 / Δt)
BL_code = min(BL_code_configured, 0.018 / Δt)The default FLL-assisted third-order carrier filter diverges at BL · Δt ≈ 0.4 (the plain third-order one at ≈ 0.43). Below that, the loop's actual noise bandwidth runs wider than configured:
BL · Δt | 0.018 | 0.036 | 0.072 | 0.09 | 0.18 | 0.36 |
|---|---|---|---|---|---|---|
ThirdOrderAssistedBilinearLF | 1.05× | 1.09× | 1.19× | 1.25× | 1.65× | 5.2× |
ThirdOrderBilinearLF | 0.97× | 1.00× | 1.07× | 1.12× | 1.38× | 2.8× |
The PLL cap of 0.09 keeps the loop within 25 % of its configured bandwidth with about 4× stability margin, at the wide and the narrow bandwidth alike. It is the product of Kaplan & Hegarty's third-order design example (18 Hz at 5 ms) and matches GNSS-SDR's narrow post-sync bandwidths (5 Hz at 20 ms). A tighter cap on the narrow bandwidth would buy little: at 20 ms and 30 dB-Hz the thermal jitter is about 4° at 4.5 Hz and 2.6° at 2 Hz, while a TCXO's jitter (Allan deviation 1e-10, GPS L1) grows from about 6° to 13° and the jerk the loop tolerates falls with BL³. From 10 ms on, the cap sets wide and narrow alike. The resulting defaults, BL in Hz:
| Records | Signals | Wide | Narrow | FLL-assist | Code |
|---|---|---|---|---|---|
| 1 ms | GPS L1 C/A, GPS L5, Galileo E5a, … | 50 | 18 | 5 | 1 |
| 2 ms | Galileo E5a-QP | 45 | 18 | 5 | 1 |
| 4 ms | Galileo E1B / E1C | 22.5 | 18 | 5 | 1 |
| 10 ms | GPS L1C, BeiDou B1C; L5I synced at 10 ms | 9 | 9 | 2 | 1 |
| 20 ms | GPS L2 CM; L1 C/A, L5Q, E5a-I synced | 4.5 | 4.5 | 1 | 0.9 |
| 1.5 s | GPS L2 CL | 0.06 | 0.06 | 0.013 | 0.012 |
The code cap of 0.018 is conservative (see MAX_CODE_LOOP_BANDWIDTH_TIME_PRODUCT); the default 1 Hz applies in full up to 18 ms.
TrackingLoops.MAX_CARRIER_LOOP_BANDWIDTH_TIME_PRODUCT — Constant
MAX_CARRIER_LOOP_BANDWIDTH_TIME_PRODUCTThe largest carrier-loop bandwidth–update interval product BL · Δt, 0.09, that effective_carrier_loop_filter_bandwidth caps at. The default FLL-assisted third-order filter diverges at BL · Δt ≈ 0.4, and at 0.09 its noise bandwidth already runs 25 % wider than configured.
TrackingLoops.MAX_CODE_LOOP_BANDWIDTH_TIME_PRODUCT — Constant
MAX_CODE_LOOP_BANDWIDTH_TIME_PRODUCTThe largest code-loop bandwidth–update interval product BL · Δt, 0.018, that effective_code_loop_filter_bandwidth caps at. Conservative: the second-order code filter destabilizes only around BL · Δt ≈ 0.4 (Stephens & Thomas 1995, "Controlled-Root Formulation for Digital Phase-Locked Loops", IEEE Trans. AES 31(1)).
TrackingLoops.MAX_FLL_ASSIST_LOOP_BANDWIDTH_TIME_PRODUCT — Constant
MAX_FLL_ASSIST_LOOP_BANDWIDTH_TIME_PRODUCTThe largest bandwidth–update interval product BL · Δt of an FLL-assisted carrier filter's FLL path, 0.02, that effective_fll_assist_loop_filter_bandwidth caps at: 5 Hz up to 4 ms records, 2 Hz at 10 ms.
TrackingLoops.LoopBandwidths — Type
LoopBandwidths(; wide_carrier = 50Hz, narrow_carrier = 18Hz, fll_assist = 5Hz,
code = 1Hz)
LoopBandwidths(signal; wide_carrier, narrow_carrier, fll_assist, code)A scalar estimator's per-satellite bandwidths: the PLL's before and after it narrows, the FLL path's while FLL-assisted (see CarrierLoopStage), and the DLL's. Given a signal, each one not given is the signal's default_*_loop_filter_bandwidth, so methods of those for a signal type apply. init_estimator_state resolves them from the estimator's, each nothing there from the driver signal's default. Each is capped at filter time (effective_carrier_loop_filter_bandwidth, effective_fll_assist_loop_filter_bandwidth, effective_code_loop_filter_bandwidth).
TrackingLoops.aid_dopplers — Method
aid_dopplers(
signal,
init_carrier_doppler,
init_code_doppler,
carrier_freq_update,
code_freq_update
)
Add the loop-filter updates to the initial Dopplers, carrier-aiding the code Doppler: the carrier update times the code-to-carrier frequency ratio is added to it.
TrackingLoops.calculate_code_frequency_update — Method
calculate_code_frequency_update(signal, code_loop_filter, correlator, code_doppler,
sampling_frequency, integration_time, loop_bandwidth)
-> (code_freq_update, code_loop_filter)One code-loop step: the DLL discriminator (dll_disc) of correlator filtered by code_loop_filter at loop_bandwidth. code_doppler, the replica's, converts the tap spacing from samples to chips. Returns the code-frequency correction (before aid_dopplers) and the advanced filter.
TrackingLoops.default_code_loop_filter_bandwidth — Method
default_code_loop_filter_bandwidth(signal)
Recommended code-loop-filter (DLL) bandwidth for signal: 1 Hz, within the 0.25–2 Hz of GNSS-SDR, SoftGNSS and PocketSDR. Carrier-aided (see aid_dopplers), the DLL has almost no dynamics left, so this is a noise-versus-pull-in trade independent of the signal. Capped at filter time by effective_code_loop_filter_bandwidth.
Override by defining a method for your signal type.
TrackingLoops.default_fll_assist_loop_filter_bandwidth — Method
default_fll_assist_loop_filter_bandwidth(signal)
Recommended FLL-assist bandwidth for signal: 5 Hz, the noise bandwidth of the second-order FLL path of an FLL-assisted carrier filter while it is FLL-assisted (the fll half of its (pll, fll) bandwidth pair). Capped at filter time by effective_fll_assist_loop_filter_bandwidth.
Override by defining a method for your signal type, or pass fll_assist_loop_filter_bandwidth = to the estimator.
TrackingLoops.default_narrow_carrier_loop_filter_bandwidth — Method
default_narrow_carrier_loop_filter_bandwidth(signal)
Recommended narrow (tracking) carrier-loop-filter bandwidth for signal: 18 Hz, the third-order PLL bandwidth of the literature (Kaplan & Hegarty, Pany), which the loop narrows to once phase lock has held (see CarrierLoopStage). Capped at filter time by effective_carrier_loop_filter_bandwidth: 18 Hz up to 5 ms, 9 Hz at 10 ms, 4.5 Hz at 20 ms.
Override by defining a method for your signal type, or pass narrow_carrier_loop_filter_bandwidth = to the estimator.
TrackingLoops.default_wide_carrier_loop_filter_bandwidth — Method
default_wide_carrier_loop_filter_bandwidth(signal)
Recommended wide (pull-in) carrier-loop-filter bandwidth for signal: 50 Hz, the third-order PLL's noise bandwidth until phase lock has held (see CarrierLoopStage). Capped at filter time by effective_carrier_loop_filter_bandwidth: 50 Hz at 1 ms, 22.5 Hz at 4 ms, 9 Hz at 10 ms. NCOReferencedPLLAndDLL does not read it (its wide default is the narrow one).
Override by defining a method for your signal type, or pass wide_carrier_loop_filter_bandwidth = to the estimator.
TrackingLoops.effective_carrier_loop_filter_bandwidth — Method
effective_carrier_loop_filter_bandwidth(
bandwidth,
integration_time
)
PLL bandwidth for a record of integration_time, wide or narrow alike: bandwidth capped at MAX_CARRIER_LOOP_BANDWIDTH_TIME_PRODUCT / integration_time for stability on long integrations. Only ever narrows, an explicit bandwidth as much as a default.
TrackingLoops.effective_code_loop_filter_bandwidth — Method
effective_code_loop_filter_bandwidth(
bandwidth,
integration_time
)
Code-loop bandwidth for a record of integration_time: bandwidth capped at MAX_CODE_LOOP_BANDWIDTH_TIME_PRODUCT / integration_time. Integration time does not otherwise narrow the DLL, as neither its dynamics nor its noise floor depend on it. The default 1 Hz is capped only past 18 ms (0.9 Hz at 20 ms).
TrackingLoops.effective_fll_assist_loop_filter_bandwidth — Method
effective_fll_assist_loop_filter_bandwidth(
bandwidth,
integration_time
)
FLL-assist bandwidth for a record of integration_time: bandwidth capped at MAX_FLL_ASSIST_LOOP_BANDWIDTH_TIME_PRODUCT / integration_time.
Integration length
TrackingLoops.calc_num_code_blocks_for_bit_buffer — Method
calc_num_code_blocks_for_bit_buffer(
signal,
integrated_samples,
sampling_frequency,
secondary_code_or_bit_found
)
Number of primary code blocks to credit the bit-buffer accumulator with for a just-completed integration: always 1 before bit/secondary sync (the detectors shift exactly one prompt sign per call), and afterwards the whole blocks the record actually covered, recovered from its sample count.
TrackingLoops.calc_num_code_blocks_to_integrate — Method
calc_num_code_blocks_to_integrate(
signal,
preferred_num_code_blocks,
secondary_code_or_bit_found
)
Returns the appropriate number of code blocks to integrate. It will be just a single code block as long as the secondary code or bit hasn't been found. Once found, the coherent integration is capped by max_num_code_blocks_to_integrate and clamped to the largest divisor of that ceiling not exceeding the preferred value, so an integration never straddles a symbol boundary.
TrackingLoops.default_num_code_blocks_to_integrate — Method
default_num_code_blocks_to_integrate(_)
Coherent integration length, in primary code blocks, a freshly tracked signal starts at. One block for every signal but Galileo E5a-QP, whose 64.5 µs block is too short to run a loop on and which starts at a whole 31-block code cycle.
TrackingLoops.max_num_code_blocks_to_integrate — Method
max_num_code_blocks_to_integrate(signal)
Longest coherent integration, in primary code blocks, that signal's own structure allows — the ceiling calc_num_code_blocks_to_integrate clamps a satellite's preferred integration length against.
One full symbol: the data-bit period for data-bearing signals, or the secondary-code period for pilots (data_frequency == 0, e.g. GPS L1C-P). Integrating past it would straddle a symbol boundary and average two opposite signs away.
The default degenerates to a single block for a signal that has neither a multi-block data bit nor an overlay; Galileo E5a-QP overrides it to a whole 31-block code cycle (see galileo/e5a_qp.jl). GPS L2CL is the same shape and deliberately keeps the default: its 1.5 s primary period is already a whole coherent integration.