This document records the findings, issues, and solutions encountered during integration testing of the Orientable-AI-GNSS / libgnss-recv-l1ca receiver against the project’s GPS signal generators.
Two campaigns are documented:
libgnss-tx-swift generator → receiver (initial bring-up).libgnss-gen-l1 generator → libgnss-recv-l1ca receiver (cross-repository loopback).libgnss-tx-swift → receiverlibgnss-tx-swift was used to generate a 90-second GPS L1 C/A baseband signal.gnss-sdr was used as a reference to validate the signal and standard decoding processes.Orientable-AI-GNSS was tested for end-to-end acquisition, tracking, bit synchronization, and subframe decoding.During integration testing, the target receiver successfully acquired satellites, tracked signals, and found bit synchronization. However, it repeatedly failed at the subframe decoding stage despite finding the correct preamble (10001011).
Two major bugs were identified:
Incorrect Preamble Polarity Configuration (src/core/sdrinit.c)
The GPS L1 C/A preamble array pre_l1ca was misconfigured as { 1, -1, -1, -1, 1, -1, 1, 1 }. In the internal logic where -1 represents a binary 1 and 1 represents a binary 0, this mapped to 01110100 instead of the correct GPS preamble 10001011.
As a result, when the true preamble was received, the correlation was -8, leading the receiver to wrongly flag the signal as inverted (polarity = -1). This double inversion corrupted the bit stream before the parity check.
Faulty Parity Check Implementation (src/core/coding/hamming32.c)
The original paritycheck_l1ca implementation did not correctly implement the GPS IS-GPS-200 Hamming(32, 26) parity check algorithm. Specifically, it failed to properly un-invert the parity bits ($D_{25}$ to $D_{30}$) when the 30th bit of the previous word ($D_{30}^*$) was a 1.
pre_l1ca array in sdrinit.c was updated to {-1, 1, 1, 1, -1, 1, -1, -1}, correctly matching the standard GPS preamble sequence 10001011.decode_l1ca_word function was implemented in hamming32.c and integrated into sdrnav.c. This standardizes the parity check logic, ensuring that un-inversion is applied when $D_{30}^* = 1$.Note (superseded by Campaign 2): the original fix un-inverted with the mask
0x3FFFFFFF, which flips all 30 low bits (24 data bits and 6 parity bits). Campaign 2 showed this is incorrect for words whose predecessor ends in $D_{30}=1$; the mask was corrected to0x3FFFFFC0(data bits only). See Campaign 2, Issue 2.
Following the fixes, the integration test against the 90-second generated libgnss-tx-swift signal was successful. Orientable-AI-GNSS successfully decoded Subframe 4 and Subframe 5 with passing parity checks.
libgnss-gen-l1 → libgnss-recv-l1calibgnss-gen-l1 (this repository’s sibling GNSS L1 baseband generator), driven by
configure/integration_test_l1ca.json: GPS L1 C/A only, ci8_le (signed 8-bit interleaved I/Q),
2.6 MHz sample rate, 45 s duration, static receiver position, RINEX nav rinex/brdc0010.22n
(epoch 2022-01-01, GPS week 2190).libgnss-recv-l1ca CLI (orientable-libgnss-cli), driven by
cfg/integration_test_gen_l1.yaml → cfg/front-end/integration_test_gen_l1.yaml
(FILE frontend, 2.6 MHz, data_type: 2 = complex I/Q, 32 GPS channels).The receiver acquired all visible satellites with strong tracking (C/N0 ≈ 38–50 dB-Hz) and found bit synchronization, but never decoded a subframe: the preamble was rarely detected and the parity check never reached 10/10 words (best observed was 4/10).
Debugging from the receiver side (good acquisition but garbled nav bits) and then instrumenting the generator’s transmitted nav-bit stream revealed two independent bugs, one in each repository:
Generator: navigation subframes were never populated (libgnss-gen-l1/src/mac/gps_lnav.c)
generate_gps_lnav() ignored its ephemeris argument ((void)eph;) and never called
gps_lnav_eph2sbf(), so link->sbf stayed all-zeros (from calloc). Every telemetry (TLM)
word was transmitted as 0x00000000 — the preamble 10001011 simply did not exist in the
signal. The handover words (HOW) appeared non-zero only because TOW is injected directly
(sbfm_word |= tow << 13), which masked the problem in casual inspection.
This bug went unnoticed because the receiver had previously only been integration-tested against
libgnss-tx-swift (Campaign 1), not against libgnss-gen-l1.
Receiver: parity check un-inverted the parity bits (src/core/coding/hamming32.c)
decode_l1ca_word() used if (word & 0x40000000) word ^= 0x3FFFFFFF;. The mask 0x3FFFFFFF
flips all 30 low bits — the 24 data bits and the 6 parity bits. Per IS-GPS-200 the parity
bits ($D_{25}$–$D_{30}$) are transmitted un-inverted; only the 24 data bits are inverted by
$D_{30}^$. As a result, every word whose predecessor ended in $D_{30}=1$ failed parity.
The existing unit test (Hamming32Test.ParityChecksumMatches) only exercised the $D_{30}^=0$
case, so the bug was latent. (This is the mask that Campaign 1’s note above flagged as
superseded.)
libgnss-gen-l1): thread the ionospheric/UTC parameters through the nav-message
path and populate the subframes at initialization:
generate_nav_msg() and generate_gps_lnav() gained a const Ionoutc_t *ionoutc parameter
(include/link.h, include/gps_lnav.h, src/mac/link.c).generate_gps_lnav() now calls gps_lnav_eph2sbf(link->sbf, *eph, *ionoutc) when init == 1
(guarded on non-null pointers so tests that pre-fill sbf still work).src/simulator_control.c (passes &ctx->ionoutc) and
src/scheduling/link_scheduler.c (passes &ionoutc); tests/unit/test_link.c updated for the
new signature.
After the fix the TLM words transmit as 0x22C00012 (preamble 0x8B + parity) and the full
subframe 1–5 content is present.libgnss-recv-l1ca): in decode_l1ca_word(), un-invert only the 24 data bits:
if (word & 0x40000000) word ^= 0x3FFFFFC0; (was 0x3FFFFFFF). The 6 parity bits are left
untouched, matching IS-GPS-200 and the shared encoder convention used by both repositories.ctest — 30/30 pass (including test_link, test_checksum).Hamming32Test — 3/3 pass, including a new regression test
Hamming32Test.RoundTripWithD30StarSet that round-trips encode→decode for $D_{30}^* \in {0,1}$
and fails against the old 0x3FFFFFFF mask.# 1. Generate the signal (writes output/integration_test_l1ca.bin)
cd <libgnss-gen-l1>
./gen-l1-signals -c configure/integration_test_l1ca.json
# 2. Run the receiver on the generated file
cd <libgnss-recv-l1ca>
./build/orientable-libgnss-cli cfg/integration_test_gen_l1.yaml < /dev/null
# 3. Inspect decoded subframes
grep -E "ID=[1-5] tow" log/integration_test.log
A one-time double initialization call in the generator stretches the first ~200 ms of nav bits (the first 5 bits are emitted twice). The receiver recovers bit synchronization immediately afterwards and decodes subsequent subframes normally; it does not affect the loopback result.
Following the CU-SDR comparative architecture review (see
docs/developer-guide/cu-sdr-comparison-review.md), three algorithm improvements were ported
from the CU-SDR MATLAB reference into the C receiver:
fine_acquisition() in sdracq.c): 40 ms coherent integration
with 25 Hz frequency bins and 20 nav-bit-edge hypotheses, reducing frequency handoff error
from ±100 Hz to ±12.5 Hz.cn0_vsm() in sdrtrk.c): Variance Summing Method replacing the
undocumented heuristic formula, providing front-end-independent signal quality metrics.lockdetect() in sdrtrk.c): carrier lock indicator
using normalized |IP|/mag ratio, with Doppler-predicted vs. measured carrier phase comparison
for cycle-slip detection. LLI flags propagated through sdrobs_t to RINEX output.libgnss-gen-l1, same config as Campaign 2
(configure/integration_test_l1ca.json: GPS L1 C/A, ci8_le, 2.6 MHz, 45 s, static).libgnss-recv-l1ca CLI with all three improvements active,
same config (cfg/integration_test_gen_l1.yaml).| Metric | Count |
|---|---|
| Satellites acquired | 13 |
| Bit syncs found | 9 |
| Preambles locked | 2 (G03, G08) |
| Subframes decoded | 7 |
| Parity failures (transient) | 36 |
| Cycle slips detected | 6 |
| Satellite | Subframe ID | TOW (s) | Week |
|---|---|---|---|
| G08 | 4 | 518424 | — |
| G08 | 5 | 518430 | — |
| G08 | 1 | 518436 | 2190 |
| G08 | 2 | 518442 | 2190 |
| G03 | 5 | 518430 | — |
| G03 | 1 | 518436 | 2190 |
| G03 | 3 | 518442 | 2190 |
TOW increments by 6 s per subframe — correct per IS-GPS-200. Week 2190 matches the generator’s epoch (2022-01-01). G08 decoded 4 consecutive subframes (4→5→1→2); G03 decoded 3 (5→1→3).
Fine acquisition: All 13 acquisitions used the new fine_acquisition() stage. Frequency
estimates at 25 Hz resolution (e.g., G01: −75 Hz, G10: +100 Hz, G16: +275 Hz). The fine
stage runs after coarse FFT detection and before tracking handoff.
VSM C/N₀: Acquisition C/N₀ values reported in the 37–52 dB-Hz range, consistent with the generator’s 15 dB SNR setting and the expected processing gain. Values are stable across channels and physically plausible.
Lock detector: 6 cycle slips detected and logged (e.g., G07 cycle slip detected:
dL_meas=0.1911 dL_pred=0.8282), confirming the feature triggers correctly during tracking
transients. LLI flags propagated to sdrobs_t.
A standalone C program (/tmp/parity_e2e.c) was written to verify end-to-end parity
compatibility between the generator’s calc_hamming32_checksum_v0() and the receiver’s
decode_l1ca_word(). The test:
findpreamble with corr=−8)Result: All 20 words across 2 subframes pass — the algorithms are provably compatible. The 36 transient parity failures in the live integration test are caused by the tracking loop needing ~8 seconds to converge (PLL/FLL pull-in from residual acquisition frequency), not by any algorithmic mismatch.
# 1. Generate the signal (writes output/integration_test_l1ca.bin)
cd <libgnss-gen-l1>
./gen-l1-signals -c configure/integration_test_l1ca.json
# 2. Run the receiver on the generated file
cd <libgnss-recv-l1ca>
./build/orientable-libgnss-cli cfg/integration_test_gen_l1.yaml < /dev/null
# 3. Inspect decoded subframes
grep "ID=" log/integration_test.log
# 4. Run unit tests (103 tests)
cd build && ./unit_tests
The Campaign 2/3 loopback is automated by
<libgnss-gen-l1>/tests/validation/closed_loop_receiver_test.py:
configure/integration_test_l1ca.json →
output/integration_test_l1ca.bin, 234 MB of ci8 @ 2.6 MHz).orientable-libgnss-cli cfg/integration_test_gen_l1.yaml on it.log/integration_test.log:
flagacq = 1)cd <libgnss-gen-l1>
python3 tests/validation/closed_loop_receiver_test.py # full run (~7 min)
python3 tests/validation/closed_loop_receiver_test.py --reuse-signal # iterate on receiver side
Exit codes: 0 pass, 1 fail, 2 prerequisites missing (both build/gen-l1-signals
and build/orientable-libgnss-cli must exist; the receiver repository is located
as a sibling directory or via LIBGNSS_RECV_L1CA).
Reference run (2026-08-03): 13 acquisitions (C/N0 44–52 dB-Hz), 8 bit syncs, 6 valid subframes across G01/G03 with week 2190 and TOWs 518424–518442, 0 anomalous decodes.