Bit and secondary-code synchronisation
Until the navigation-bit boundary (for a data signal) or the secondary-code phase (for a pilot) is known, a signal can only be integrated over one primary code period. The BitBuffer collects the prompts of those short integrations, and a per-signal detector (detect_bit_or_secondary_code_sync) searches them for the boundary. Once has_bit_or_secondary_code_been_found returns true, longer coherent integrations become available (see calc_num_code_blocks_to_integrate) and the buffer accumulates the decoded soft bits, which get_soft_bits returns.
Detectors exist for every GPS, Galileo and BeiDou signal that GNSSSignals.jl models. How a signal is synchronised is a method of detect_bit_or_secondary_code_sync and of the small trait functions below (uses_soft_bit_edge_detection, get_bit_edge_or_secondary_code_tolerance, …), so a new signal type can be supported by adding methods for it.
TrackingLoops.BitBuffer — Type
BitBuffer to buffer bits.
The code_block_buffer field is the sync-search sliding window — its width B is chosen per signal by get_code_block_buffer_type so that a single integer can hold the entire pre-sync search horizon (one NH10 period for GPS L5I, 40 primary blocks for GPS L1 C/A, 1800 chips for the GPS L1C-P overlay, etc.). After sync the field is dead state and the decoded navigation bits accumulate as soft bits in soft_bits (one polarity-corrected coherent prompt sum per bit, sign = the hard bit). Soft bits are the only bit store — decoders take them directly (soft-decision Viterbi for Galileo E1B, LDPC for GPS L1C-D, confidence weighting everywhere else), and a hard decision is just soft_bit > 0. Because the store is a growable vector rather than the fixed UInt128 it used to be, there is no limit on how many bits may accumulate between resets.
The phase_acc field holds the incremental per-hypothesis bin statistics (PhaseAccumulators) consumed by whichever soft CFAR sync detector the signal uses — _detect_bit_edge_cfar for signals whose uses_soft_bit_edge_detection is true (GPS L1 C/A), or _detect_secondary_code_cfar for those whose uses_soft_secondary_code_detection is true (every signal with a secondary code of length 1 < N ≤ 100). It is seeded and updated only for those signals; for all others (hard-decision path) it stays empty. Its size is bounded (one entry per hypothesis), so there is no growing pre-sync history.
TrackingLoops.PhaseAccumulators — Type
Per-hypothesis bin statistics for the soft, maximum-energy CFAR sync detectors — one entry per candidate timing hypothesis. It backs both soft detectors (a signal uses at most one): the GPS L1 C/A bit-edge detector _detect_bit_edge_cfar, where a hypothesis is an edge phase phase ∈ 0:blocks_per_bit-1 and the bin is one navigation bit, updated by _update_phase_accumulators!; and the secondary-code detector _detect_secondary_code_cfar, where a hypothesis is an overlay rotation d ∈ 0:N-1 and the bin is one (overlay-wiped) secondary-code period, updated by _update_secondary_accumulators!. In both cases it is advanced one primary-code block at a time so detection stays O(hypotheses) per block with no growing pre-sync history. Below, period is blocks_per_bit (L1 C/A) or the secondary-code length N.
The vectors are mutated in place across the immutable BitBuffer reconstructions (the same pattern as soft_bits). This is deliberate: the accumulators are streaming state advanced every block, and an immutable representation would either inline ~660 B into every per-block BitBuffer copy (≈6× the heap traffic, dead weight post-sync) or box a fresh value each block — both reintroduce the per-block allocation this design exists to avoid. A single shared, in-place-updated buffer per satellite is the allocation-free choice.
Fields (all length period once seeded; empty before the first block):
open_bin_sum— coherent sum of the hypothesis's currently open bin (overlay-wiped for the secondary-code detector).mean_bin_energy/bin_energy_sum_of_squared_deviations— Welford running mean and sum of squared deviations (M₂ = Σ(energyᵢ − mean)²) of the hypothesis's completed-bin energies, for a numerically stable variance. The bin count is not stored — it isdiv(num_blocks - hypothesis, period).last_bin_polarity— sign (±1,0before the first bin) of the most recently completed bin's real part, i.e. the lock polarity.
TrackingLoops.SyncResult — Type
SyncResultOutcome of a per-signal bit-sync / secondary-code-sync detector call.
Fields:
found::Bool— whether the detector locked on this update.phase::Int— whenfound = true, the secondary-code chip the upcoming integration aligns to, in0:secondary_code_length-1(recovered by the hard rotation search in_secondary_code_search). Zero for signals without a secondary code (the L1 C/A bit-edge case fires at the data-bit boundary, where the upcoming integration starts a new bit, not at a secondary-chip offset) and also for the soft_detect_secondary_code_cfar, which only fires at the winning rotation's own period boundary, so the upcoming integration always starts at secondary chip 0.polarity::Int8—+1or-1; which match orientation the detector locked. Carries through to the post-sync prompt accumulator so that a negative-polarity lock doesn't trip the downstream bit decoder.
TrackingLoops.get_bit_edge_detection_confidence — Method
get_bit_edge_detection_confidence(_)
Target confidence (one minus the probability of a false lock) for the soft-decision CFAR sync detectors — the GPS L1 C/A bit-edge detector _detect_bit_edge_cfar and the secondary-code detector _detect_secondary_code_cfar both read it.
Default 0.999: the detector keeps integrating primary-code blocks until the maximum-energy hypothesis beats its closest competitor with this confidence, so a clean signal locks in as little as two bins while a noisy one self-paces to as long as it takes. Lower it to lock faster at the cost of more false locks; raise it to be more conservative.
Overriding
TrackingLoops.get_bit_edge_detection_confidence(::GPSL1CA) = 0.9999Takes effect at the next detector call — no TrackState rebuild needed.
TrackingLoops.get_bit_edge_or_secondary_code_tolerance — Method
get_bit_edge_or_secondary_code_tolerance(_)
Per-signal Hamming tolerance used by the hard-decision rotation/Hamming sweep _secondary_code_search, expressed as a fraction of the search window.
Returns the largest fraction of bit-flips the per-signal detect_bit_or_secondary_code_sync accepts before reporting found = true. Each detector converts this to an integer error budget at its call site: max_errors = floor(Int, tolerance × window_size).
Among the currently implemented signals only the 1800-chip overlay pilots GPS L1C-P and BeiDou B1C-P read this trait — they are the only signals still on the hard path. The short-secondary-code signals (GPS L5I/L5Q, Galileo E1C/E5aI/E5aQ/E5bI/E5bQ/E6C, BeiDou B1I/B3I/B2aI/B2aQ) were moved to the soft, confidence-driven _detect_secondary_code_cfar (selected by uses_soft_secondary_code_detection) and no longer consult it; likewise GPS L1 C/A uses _detect_bit_edge_cfar. Both soft detectors are tuned by get_bit_edge_detection_confidence instead. Galileo E1B / E6-B, GPS L1C-D and BeiDou B2b-I / B1C-D broadcast one channel symbol per primary code period, so their detectors return SyncResult(true, 0, +1) unconditionally — the trait default applies but the value is ignored — and GPS L2CL is a dataless pilot with no sync.
Default is 0.025 (2.5 %). At the 1800-chip window of either overlay pilot that discretizes to max_errors = 45. (For reference, the same 2.5 % at the now-soft signals' short windows would floor to 0–2 errors — an exact or near-exact match — which is exactly the noise-driven false-lock exposure the move to the soft detector removed.)
Overriding
To loosen the tolerance for low-C/N₀ work, dispatch the trait on the (hard-path) signal type in your own module:
TrackingLoops.get_bit_edge_or_secondary_code_tolerance(::GPSL1C_P) = 0.05The override takes effect at the next call to detect_bit_or_secondary_code_sync — there is no need to rebuild any TrackState. The trait is @inline'd so the override folds at the detector's call site. (Overriding it for a soft-detector signal has no effect; tune get_bit_edge_detection_confidence there instead.)
TrackingLoops.get_code_block_buffer_type — Method
get_code_block_buffer_type(_)
Width B of the packed prompt-sign buffer (BitBuffer.code_block_buffer) for signal, returned as a concrete Unsigned subtype.
That packed buffer is the sliding-window search horizon only for the hard-decision path — the rotation/Hamming sweep _secondary_code_search, which among the currently implemented signals is used by the two 1800-chip overlay pilots, GPS L1C-P and BeiDou B1C-P. The soft-decision CFAR detectors (_detect_bit_edge_cfar for GPS L1 C/A, _detect_secondary_code_cfar for the short-secondary-code signals) read the incremental PhaseAccumulators instead, so for those signals the packed buffer of this width is built but not consulted for detection — it is vestigial (the width could be UInt8; it is left at the horizon width below for uniformity and so the hard path stays available). The table gives, per signal, the returned width and — for the hard path — the horizon it must hold:
| Signal | Returns | Detector / buffer role |
|---|---|---|
| GPS L1 C/A | UInt64 | soft bit-edge CFAR — packed buffer vestigial |
| Galileo E1B | UInt8 | symbol = primary period, buffer unused |
| GPS L5I | UInt32 | soft secondary CFAR — packed buffer vestigial |
| GPS L5Q | UInt32 | soft secondary CFAR — packed buffer vestigial |
| GPS L1C-D | UInt8 | symbol = primary period, buffer unused |
| GPS L1C-P | UInt1800 | hard rotation sweep — 1800-chip overlay horizon |
| GPS L2CM | UInt8 | symbol = primary period, buffer unused |
| GPS L2CL | UInt8 | dataless pilot, no sync, buffer unused |
| Galileo E1C | UInt32 | soft secondary CFAR — packed buffer vestigial |
| Galileo E5a-I | UInt32 | soft secondary CFAR — packed buffer vestigial |
| Galileo E5a-Q | UInt128 | soft secondary CFAR — packed buffer vestigial |
| Galileo E5b-I | UInt32 | soft secondary CFAR — packed buffer vestigial |
| Galileo E5b-Q | UInt128 | soft secondary CFAR — packed buffer vestigial |
| Galileo E6-B | UInt8 | symbol = primary period, buffer unused |
| Galileo E6-C | UInt128 | soft secondary CFAR — packed buffer vestigial |
| BeiDou B1I | UInt32 | soft secondary CFAR — packed buffer vestigial |
| BeiDou B3I | UInt32 | soft secondary CFAR — packed buffer vestigial |
| BeiDou B2a-I | UInt32 | soft secondary CFAR — packed buffer vestigial |
| BeiDou B2a-Q | UInt128 | soft secondary CFAR — packed buffer vestigial |
| BeiDou B2b-I | UInt8 | symbol = primary period, buffer unused |
| BeiDou B1C-D | UInt8 | symbol = primary period, buffer unused |
| BeiDou B1C-P | UInt1800 | hard rotation sweep — 1800-chip overlay horizon |
The default for any signal not specialized below is UInt64. The width flows through BitBuffer{B} and TrackedSignal{Sig, B, C, PCF, CN0} so the parameter chain stays type-stable at construction.
TrackingLoops.get_soft_bits — Method
get_soft_bits(bit_buffer::BitBuffer)
get_soft_bits(state::SignalLoopState)The soft bits decoded so far: for each completed navigation bit, the sum of its filtered prompts (real part, after de-rotation). A hard decision is the sign, soft_bit > 0; the magnitude is the bit's reliability.
Bits recovered from the pre-sync window at the moment of bit sync only have the ±1 signs of their prompts available; their sign-vote sum is scaled by the sync-time prompt magnitude so that their magnitudes stay comparable with the coherently accumulated post-sync bits.
This is the buffer's own vector, not a copy. The consumer drains it (empty!) after reading; it has room for 64 bits before it grows and allocates.
TrackingLoops.has_bit_or_secondary_code_been_found — Method
has_bit_or_secondary_code_been_found(bit_buffer::BitBuffer)
has_bit_or_secondary_code_been_found(state::SignalLoopState)Whether the signal's navigation-bit boundary (data signals) or secondary-code phase (pilots) has been found. From then on the signal may be integrated over more than one primary code period, and completed bits are collected for get_soft_bits.
TrackingLoops.uses_soft_bit_edge_detection — Method
uses_soft_bit_edge_detection(signal)
Whether signal's bit edge is located with the soft-decision, maximum-energy CFAR detector _detect_bit_edge_cfar (which reads the incremental PhaseAccumulators) rather than the hard-decision detect_bit_or_secondary_code_sync path.
This is signal-agnostic: the detector and accumulators are parameterised by the number of primary-code blocks per navigation bit (L, from _calc_num_code_blocks_that_form_a_bit), with no per-signal constants. The default enables it for any signal whose navigation bit spans more than one primary-code period and which carries no secondary/overlay code — i.e. the bit edge is a sub-bit timing offset to be found, not a symbol boundary that is already aligned (Galileo E1B, GPS L1C-D: one symbol per primary period) and not a periodic overlay (GPS L5I, L1C-P: located by _secondary_code_search). Among the currently implemented signals only GPS L1 C/A (20 blocks/bit) qualifies, but a newly added signal with the same structure is picked up automatically. Note BeiDou B1I/B3I do not qualify even though their GEO satellites carry no overlay: get_secondary_code_length is 20 for the signal type, so they route to the secondary-code detector. On a GEO PRN that detector finds nothing to lock — the all-ones column is rotation-invariant and the D2 symbols are 2 blocks long, so no 20-block rotation bin stands out — and those satellites stay pre-sync (see src/beidou/b1i.jl).
Override per signal type to force the choice, e.g. to disable it:
TrackingLoops.uses_soft_bit_edge_detection(::SomeSignal) = falseThe result is constant-folded per signal type, so the branch in _buffer_find_bit compiles away and signals that don't use it never seed or update phase_acc.
TrackingLoops.uses_soft_secondary_code_detection — Method
uses_soft_secondary_code_detection(signal)
Whether signal's secondary/overlay code is located with the soft-decision, maximum-energy CFAR detector _detect_secondary_code_cfar (which reads the incremental PhaseAccumulators) rather than the hard-decision rotation/Hamming sweep _secondary_code_search.
The soft detector coherently integrates one full secondary-code period per bin, so it only makes sense while a whole period is a phase-coherent integration length. The default therefore enables it for signals with a short secondary code — 1 < get_secondary_code_length(signal) ≤ 100 — which covers GPS L5I (NH10, 10), GPS L5Q (NH20, 20), Galileo E1C (CS25, 25), E5aI (CS20, 20), E5aQ (CS100, 100), E5bI (CS4, 4) and E5bQ / E6C (CS100, 100), plus BeiDou B1I / B3I (NH20, 20), B2aI (5) and B2aQ (100) — all periods of at most 100 ms. The two 1800-chip overlay pilots, GPS L1C-P and BeiDou B1C-P, are deliberately excluded: an 1800-chip overlay is an 18 s period, far too long to integrate coherently (and such a long code is not false-lock-prone), so they keep the hard _secondary_code_search.
Because it locates a periodic overlay, this is mutually exclusive with uses_soft_bit_edge_detection (which requires no secondary code); a signal routes to at most one soft detector.
Override per signal type to force the choice, e.g. to disable it:
TrackingLoops.uses_soft_secondary_code_detection(::GPSL5I) = falseThe result is constant-folded per signal type, so the branch in _buffer_find_bit compiles away and signals that don't use it never seed or update phase_acc.
TrackingLoops.get_default_correlator — Function
get_default_correlator(gpsl1)
get_default_correlator(gpsl1, num_ants)
Get the default correlator for the given GNSS system. Returns an EarlyPromptLateCorrelator for GPS L1 or a VeryEarlyPromptLateCorrelator for systems like Galileo E1B that use BOC modulation.
TrackingLoops.detect_bit_or_secondary_code_sync — Method
detect_bit_or_secondary_code_sync(
signal,
prn,
code_block_bits,
num_code_blocks
)
Symbol-sync detector for GPS L1C-D.
L1C-D broadcasts one CNAV-2 channel symbol per 10 ms primary code period (100 sps; IS-GPS-800G §3.2.3), so the buffer of primary-block signs is itself the symbol stream — there is no sub-symbol boundary to find. The detector therefore reports found = true from the very first integration, leaving downstream consumers (GNSSDecoder.jl) to resolve the residual ±1 polarity ambiguity via the CNAV-2 preamble's CRC.
Same shape as the Galileo E1B "1-block-per-symbol" case.
TrackingLoops.detect_bit_or_secondary_code_sync — Method
detect_bit_or_secondary_code_sync(
signal,
prn,
code_block_bits,
num_code_blocks
)
Secondary-code sync detector for GPS L1C-P.
L1C-P broadcasts a per-PRN 1800-chip overlay code (IS-GPS-800G §3.2.2.1.2) on top of the 10-ms primary code, giving an 18-second cycle. To lock the overlay we wait for the sliding code_block_bits window to fill to 1800 primary periods, then run a single 1800-phase shifted Hamming-distance sweep against the PRN's known overlay pattern.
Returns SyncResult(false, 0, 0) until 1800 blocks have been buffered. Once that horizon is reached the generic _secondary_code_search rotation sweep picks the alignment whose Hamming distance to the overlay (or its negation) is minimal; if that distance is within the 2.5 % tolerance (≤ 45 errors) it reports SyncResult(true, phase, ±1) where phase is the secondary-chip offset of the upcoming integration, which downstream code uses to anchor the shared sat.code_phase.
TrackingLoops.detect_bit_or_secondary_code_sync — Method
detect_bit_or_secondary_code_sync(_, _, _, _)
"Sync" detector for GPS L2CL — a no-op. L2CL is a dataless pilot with no secondary/overlay code and a single 767250-chip code (1.5 s period; a whole code period is the coherent-integration unit). There is no bit and no secondary code to lock, so the detector never reports found: the tracker keeps one code block per integration (the calc_num_code_blocks cap for a signal whose get_secondary_code_length is 1) and simply tracks. Returns SyncResult(false, 0, 0).
TrackingLoops.detect_bit_or_secondary_code_sync — Method
detect_bit_or_secondary_code_sync(
signal,
prn,
code_block_bits,
num_code_blocks
)
Symbol-sync detector for GPS L2CM.
L2CM broadcasts one CNAV symbol per 20 ms L2CM code period (50 sps; IS-GPS-200N §3.2.2 — the 10230-chip code at 511.5 kcps is exactly one symbol long), so the buffer of primary-block signs is itself the symbol stream — there is no sub-symbol boundary to find. The detector therefore reports found = true from the very first integration, leaving downstream consumers (GNSSDecoder.jl) to resolve the residual ±1 polarity ambiguity via the CNAV preamble's CRC.
Same shape as the GPS L1C-D / Galileo E1B "1-block-per-symbol" case.
TrackingLoops.detect_bit_or_secondary_code_sync — Method
detect_bit_or_secondary_code_sync(
signal,
prn,
code_block_bits,
num_code_blocks
)
Secondary-code sync detector for GPS L5I — the generic _detect_secondary_code_sync rotation search over the NH10 secondary code (window 10; the default 2.5 % tolerance discretizes to 0, i.e. exact match). The negated-polarity (data-bit-1) case is handled inside the search, so the detector locks after a single NH10 period in the worst case and reports the upcoming integration's NH10 chip in SyncResult.phase. Returns SyncResult.
The packed reference is derived generically from get_secondary_code (see _packed_secondary_code); no bespoke packing is needed.
TrackingLoops.detect_bit_or_secondary_code_sync — Method
detect_bit_or_secondary_code_sync(
signal,
prn,
code_block_bits,
num_code_blocks
)
Secondary-code sync detector for GPS L5Q — the generic _detect_secondary_code_sync rotation search over the NH20 secondary code (window 20; the default 2.5 % tolerance discretizes to 0, i.e. exact match). L5Q is a pilot (no navigation data); the 20-chip Neuman-Hoffman overlay is the only sync feature, so the detector locks after a single NH20 period in the worst case and reports the upcoming integration's NH20 chip in SyncResult.phase. Returns SyncResult.
The packed reference is derived generically from get_secondary_code (see _packed_secondary_code); no bespoke packing is needed.
TrackingLoops.detect_bit_or_secondary_code_sync — Method
detect_bit_or_secondary_code_sync(
signal,
prn,
code_block_bits,
num_code_blocks
)
Symbol-sync detector for Galileo E1B (both GalileoE1B and the BOC(1,1) approximation GalileoE1B_BOC11).
E1B broadcasts one I/NAV channel symbol per 4 ms primary code period (250 sym/s; Galileo OS SIS ICD Table 11 — symbol period = primary code period, Table 15 — 4092 chips at 1.023 Mcps with no secondary code), so the buffer of primary-block signs is itself the symbol stream — there is no sub-symbol boundary to find. The detector therefore reports found = true from the very first integration, leaving downstream consumers (GNSSDecoder.jl) to resolve the residual ±1 polarity ambiguity via the I/NAV preamble.
Same shape as the GPS L1C-D "1-block-per-symbol" case.
TrackingLoops.detect_bit_or_secondary_code_sync — Method
detect_bit_or_secondary_code_sync(
signal,
prn,
code_block_bits,
num_code_blocks
)
Secondary-code sync detector for Galileo E1C (both GalileoE1C and the BOC(1,1) approximation GalileoE1C_BOC11).
E1C is the E1 pilot channel: no navigation data, but a 25-chip CS25 secondary code (Galileo OS SIS ICD Table 4) overlaid on the 4 ms primary code period, giving a 100 ms cycle. The generic _detect_secondary_code_sync rotation search locks after a single CS25 period in the worst case (default 2.5 % tolerance discretizes to 0, i.e. exact match over the 25-chip window) and reports the upcoming integration's CS25 chip in SyncResult.phase. The packed reference comes from the generic _packed_secondary_code. Returns SyncResult.
TrackingLoops.detect_bit_or_secondary_code_sync — Method
detect_bit_or_secondary_code_sync(
signal,
prn,
code_block_bits,
num_code_blocks
)
Secondary-code sync detector for Galileo E5a-I — the generic _detect_secondary_code_sync rotation search over the 20-chip CS20 secondary code (Galileo OS SIS ICD Table 19) overlaid on the 1 ms primary code period. E5a-I carries the F/NAV data stream at 50 sps, so one CS20 period (20 primary blocks) is exactly one channel symbol: the detector locks the secondary phase, and data-bit decoding then integrates one CS20 period per symbol. Default 2.5 % tolerance discretizes to 0 (exact match over the 20-chip window). The packed reference comes from the generic _packed_secondary_code. Returns SyncResult.
TrackingLoops.detect_bit_or_secondary_code_sync — Method
detect_bit_or_secondary_code_sync(
signal,
prn,
code_block_bits,
num_code_blocks
)
Secondary-code sync detector for Galileo E5a-Q — the generic _detect_secondary_code_sync rotation search over the per-PRN 100-chip CS100 secondary code (Galileo OS SIS ICD Table 20) overlaid on the 1 ms primary code period, giving a 100 ms cycle. E5a-Q is a dataless pilot; the CS100 overlay is its only sync feature, so the detector locks after a single CS100 period in the worst case and reports the upcoming integration's CS100 chip in SyncResult.phase. The per-PRN packed reference comes from the generic _packed_secondary_code, which reads the signal's PerPRNSecondaryCode. Returns SyncResult.
TrackingLoops.detect_bit_or_secondary_code_sync — Method
detect_bit_or_secondary_code_sync(
signal,
prn,
code_block_bits,
num_code_blocks
)
"Sync" detector for Galileo E5a-QP — reports found = true from the very first integration.
E5a-QP is dataless (get_data_frequency == 0) and carries no overlay (get_secondary_code is NoSecondaryCode), so there is no bit edge and no secondary-code phase to locate: every 330-chip primary block boundary is equivalent, and the only thing "sync" gates here is the switch from the single-block pre-sync integration to the whole-cycle one (max_num_code_blocks_to_integrate). Reporting the lock immediately is therefore correct rather than optimistic — unlike the GPS L2CL case, which is the same dataless/overlay-free shape but whose 1.5 s primary period is already a whole coherent integration, so there is nothing for a lock to unlock.
It is therefore the same "fires immediately, nothing to find" body the one-symbol-per-code-period signals use, _detect_symbol_is_code_block_sync: phase is 0 (no secondary code to index) and polarity is +1 (with no data and no overlay there is no sign convention to recover). Returns SyncResult.
TrackingLoops.detect_bit_or_secondary_code_sync — Method
detect_bit_or_secondary_code_sync(
signal,
prn,
code_block_bits,
num_code_blocks
)
Secondary-code sync detector for Galileo E5b-I — the generic _detect_secondary_code_sync rotation search over the 4-chip CS4 secondary code (1110, shared across all SVIDs; Galileo OS SIS ICD v2.2 §3.5.1) overlaid on the 1 ms primary code period. E5b-I carries the I/NAV data stream at 250 sym/s, so one CS4 period (4 primary blocks) is exactly one channel symbol: the detector locks the secondary phase, and data-bit decoding then integrates one CS4 period per symbol. This is the same "overlay period = one symbol" shape as Galileo E5a-I's CS20 at 50 sym/s, scaled to E5b-I's five-times-faster symbol rate.
At the default 2.5 % tolerance the hard-path error budget discretizes to 0 (exact match over the 4-chip window), but with N = 4 the trait default routes E5b-I to the soft, CFAR detector (uses_soft_secondary_code_detection) instead — a 4-chip hard template match would be badly false-lock-prone. The packed reference comes from the generic _packed_secondary_code. Returns SyncResult.
TrackingLoops.detect_bit_or_secondary_code_sync — Method
detect_bit_or_secondary_code_sync(
signal,
prn,
code_block_bits,
num_code_blocks
)
Secondary-code sync detector for Galileo E5b-Q — the generic _detect_secondary_code_sync rotation search over the per-SVID 100-chip CS100 secondary code (Galileo OS SIS ICD v2.2 §3.5.2, the CS100 codes 51-100 assigned CS100₍ₙ₊₅₀₎ to SVID n) overlaid on the 1 ms primary code period, giving a 100 ms cycle. E5b-Q is a dataless pilot; the CS100 overlay is its only sync feature, so the detector locks after a single CS100 period in the worst case and reports the upcoming integration's CS100 chip in SyncResult.phase. The per-PRN packed reference comes from the generic _packed_secondary_code, which reads the signal's PerPRNSecondaryCode — the same shape as Galileo E5a-Q, which draws the other half (CS100₁₋₅₀) of the same table. With N = 100 the trait default routes E5b-Q to the soft _detect_secondary_code_cfar (uses_soft_secondary_code_detection), so this method is reached only if a caller forces the hard path. Returns SyncResult.
TrackingLoops.detect_bit_or_secondary_code_sync — Method
detect_bit_or_secondary_code_sync(
signal,
prn,
code_block_bits,
num_code_blocks
)
Symbol-sync detector for Galileo E6-B.
E6-B broadcasts one C/NAV channel symbol per 1 ms primary code period (1000 sym/s; Galileo OS SIS ICD v2.2 Table 5 — 5115 chips at 5.115 Mcps with no secondary code, so the symbol period is the primary code period), so the buffer of primary-block signs is itself the symbol stream — there is no sub-symbol boundary to find. The detector therefore reports found = true from the very first integration, leaving downstream consumers (GNSSDecoder.jl) to resolve the residual ±1 polarity ambiguity via the C/NAV preamble.
Same shape as the Galileo E1B and GPS L1C-D "1-block-per-symbol" cases, at E6-B's four-times-faster symbol rate.
TrackingLoops.detect_bit_or_secondary_code_sync — Method
detect_bit_or_secondary_code_sync(
signal,
prn,
code_block_bits,
num_code_blocks
)
Secondary-code sync detector for Galileo E6-C — the generic _detect_secondary_code_sync rotation search over the per-SVID 100-chip CS100 secondary code (Galileo E6-B/C Codes Technical Note §2.4, the OS SIS ICD's CS100₁₋₅₀ assigned CS100ₙ to SVID n) overlaid on the 1 ms primary code period, giving a 100 ms cycle. E6-C is a dataless pilot; the CS100 overlay is its only sync feature, so the detector locks after a single CS100 period in the worst case and reports the upcoming integration's CS100 chip in SyncResult.phase. The per-PRN packed reference comes from the generic _packed_secondary_code, which reads the signal's PerPRNSecondaryCode — E6-C draws the same CS100₁₋₅₀ half of the table as GalileoE5aQ. With N = 100 the trait default routes E6-C to the soft _detect_secondary_code_cfar (uses_soft_secondary_code_detection), so this method is reached only if a caller forces the hard path. Returns SyncResult.
TrackingLoops.detect_bit_or_secondary_code_sync — Method
detect_bit_or_secondary_code_sync(
signal,
prn,
code_block_bits,
num_code_blocks
)
Secondary-code sync detector for BeiDou B1I — the generic _detect_secondary_code_sync rotation search over the per-PRN 20-chip Neuman-Hoffman overlay (NH20; BDS-SIS-ICD-B1I-3.0 §5.2.1) overlaid on the 1 ms primary code period, giving a 20 ms tiered code. On the MEO/IGSO satellites (PRN 6-58) that carry NH20, one overlay period is exactly one D1 data symbol at 50 sym/s: the detector locks the secondary phase, and data-bit decoding then integrates one NH20 period per symbol.
On the GEO satellites (PRN 1-5, 59-63) the overlay column is all-ones — they carry no NH20, and they are also the D2 satellites — so there is nothing for the rotation search to lock and the soft CFAR detector does not sync at all (see the file header). Those satellites track and range, but stay pre-sync.
At runtime this method is reached only if a caller forces B1I onto the hard path; the live detector is the soft _detect_secondary_code_cfar, which uses_soft_secondary_code_detection selects for a 20-chip overlay. Both read the same per-PRN reference — this one via the generic _packed_secondary_code, which reads the signal's PerPRNSecondaryCode. Returns SyncResult.
TrackingLoops.detect_bit_or_secondary_code_sync — Method
detect_bit_or_secondary_code_sync(
signal,
prn,
code_block_bits,
num_code_blocks
)
Secondary-code sync detector for BeiDou B3I — the generic _detect_secondary_code_sync rotation search over the per-PRN 20-chip Neuman-Hoffman overlay (NH20; BDS-SIS-ICD-B3I-1.0 §5.2.1) overlaid on the 1 ms primary code period, giving a 20 ms tiered code. The detector behaves exactly as BeiDouB1I's — one NH20 period is one D1 symbol on the MEO/IGSO satellites (PRN 6-58), while the GEO satellites' all-ones column, at their 2-block D2 symbol rate, leaves nothing for any rotation to lock so they never sync. As for B1I, the live detector is the soft _detect_secondary_code_cfar and this method is reached only if a caller forces B3I onto the hard path; the per-PRN packed reference comes from the generic _packed_secondary_code either way. Returns SyncResult.
TrackingLoops.detect_bit_or_secondary_code_sync — Method
detect_bit_or_secondary_code_sync(
signal,
prn,
code_block_bits,
num_code_blocks
)
Secondary-code sync detector for the BeiDou B2a data component — the generic _detect_secondary_code_sync rotation search over the 5-chip secondary code (00010, shared across all PRNs; BDS-SIS-ICD-B2a-1.0 §5.2.1) overlaid on the 1 ms primary code period. B2a carries B-CNAV2 at 200 sym/s, so one secondary period (5 primary blocks) is exactly one channel symbol: the detector locks the secondary phase, and data-bit decoding then integrates one secondary period per symbol — the same "overlay period = one symbol" shape as Galileo E5a-I's CS20 and E5b-I's CS4.
With N = 5 the trait default routes B2a data to the soft, CFAR detector (uses_soft_secondary_code_detection); a 5-chip hard template match would be badly false-lock-prone. The packed reference comes from the generic _packed_secondary_code. Returns SyncResult.
TrackingLoops.detect_bit_or_secondary_code_sync — Method
detect_bit_or_secondary_code_sync(
signal,
prn,
code_block_bits,
num_code_blocks
)
Secondary-code sync detector for the BeiDou B2a pilot component — the generic _detect_secondary_code_sync rotation search over the per-PRN 100-chip secondary code (truncated length-1021 Weil codes, BDS-SIS-ICD-B2a-1.0 §5.2.1 Table 5-4) overlaid on the 1 ms primary code period, giving a 100 ms cycle. The B2a pilot is dataless; the overlay is its only sync feature, so the detector locks after a single overlay period in the worst case and reports the upcoming integration's secondary chip in SyncResult.phase. The per-PRN packed reference comes from the generic _packed_secondary_code, which reads the signal's PerPRNSecondaryCode — the same shape as Galileo E5a-Q / E5b-Q / E6-C. With N = 100 the trait default routes the pilot to the soft _detect_secondary_code_cfar (uses_soft_secondary_code_detection), so this method is reached only if a caller forces the hard path. Returns SyncResult.
TrackingLoops.detect_bit_or_secondary_code_sync — Method
detect_bit_or_secondary_code_sync(
signal,
prn,
code_block_bits,
num_code_blocks
)
Symbol-sync detector for BeiDou B2b_I.
B2b_I broadcasts one B-CNAV3 symbol per 1 ms primary code period (1000 sym/s; BDS-SIS-ICD-B2b-1.0 §5 — 10230 chips at 10.23 Mcps with no secondary code, so the symbol period is the primary code period), so the buffer of primary-block signs is itself the symbol stream — there is no sub-symbol boundary to find. The detector therefore reports found = true from the very first integration, leaving downstream consumers (GNSSDecoder.jl) to resolve the residual ±1 polarity ambiguity via the B-CNAV3 preamble.
Same shape as the Galileo E1B / E6-B and GPS L1C-D "1-block-per-symbol" cases.
TrackingLoops.detect_bit_or_secondary_code_sync — Method
detect_bit_or_secondary_code_sync(
signal,
prn,
code_block_bits,
num_code_blocks
)
Symbol-sync detector for the BeiDou B1C data component.
B1C data broadcasts one B-CNAV1 symbol per 10 ms primary code period (100 sym/s; BDS-SIS-ICD-B1C-1.0 Table 5-1 — 10230 chips at 1.023 Mcps with no secondary code, so the symbol period is the primary code period), so the buffer of primary-block signs is itself the symbol stream — there is no sub-symbol boundary to find. The detector therefore reports found = true from the very first integration, leaving downstream consumers (GNSSDecoder.jl) to resolve the residual ±1 polarity ambiguity via the B-CNAV1 preamble.
Exactly the GPS L1C-D case, at the same 10 ms period and 100 sym/s rate.
TrackingLoops.detect_bit_or_secondary_code_sync — Method
detect_bit_or_secondary_code_sync(
signal,
prn,
code_block_bits,
num_code_blocks
)
Secondary-code sync detector for the BeiDou B1C pilot component.
B1C pilot broadcasts a per-PRN 1800-chip overlay code (BDS-SIS-ICD-B1C-1.0 §5.2.2, truncated Weil codes) on top of the 10 ms primary code, giving an 18 second cycle — dimensionally the same overlay as GPS L1C-P's, on the same carrier. The generic _detect_secondary_code_sync waits for the sliding code_block_bits window to fill to 1800 primary periods, then runs a single 1800-phase shifted Hamming-distance sweep (_secondary_code_search) against the PRN's overlay pattern, accepting the best alignment within the 2.5 % tolerance (get_bit_edge_or_secondary_code_tolerance, 45 errors here) and reporting the secondary-chip offset of the upcoming integration in SyncResult.phase.
Returns SyncResult(false, 0, 0) until 1800 blocks have been buffered. Like GPS L1C-P's, the overlay reaches the sweep through the generic _packed_secondary_code: GNSSSignals exposes it as a PerPRNSecondaryCode, and the per-chip packing cost is negligible next to the 1800-phase sweep that follows.