Files
gps/soc/software/gps_system/tests/test_gps_lnav_iode.c
T
onur f59d6ebb43 fix(gps): correct week-number handling across almanac, ephemeris and flash
Persisting navigation data changed its lifetime from one power cycle to
weeks, and every piece of arithmetic that had only ever run over the
shorter lifetime turned out to be wrong. Each fix below reproduces in a
host test that fails against the previous behaviour.

Almanac (gps_almanac.c, gps_lnav_parser.c)

WNa was lifted against gps_ephems.toe.week, a field nothing ever writes,
so any WNa above 128 underflowed to ~4.29e9. The propagation then reduced
tk with a while-loop, turning one visibility refresh into 4.29e9
iterations - hours on the target, inside a 10 s periodic call. WNa is the
week modulo 256, so this armed at GPS week 2433 and then for 127 weeks in
every 256. Now lifted against tow.week in signed arithmetic, refused if
impossible, and the propagation is closed form.

The IS-GPS-200 20.3.3.5.2.3 half-week wrap was also applied to tk
unconditionally. It is a week-crossover correction, not a general
reduction: a GPS week is 14.0384 orbital periods, so aliasing one week
moves a satellite 5161 km, enough to report one 5.1 degrees below the
horizon as 5.7 above. The crossover guess now lives in
gps_almanac_assumed_week(), used only before page 25 arrives, and an
almanac too old to propagate yields no prediction rather than a wrong one.

Ephemeris age (gps_ephemeris_validity.c, gps_lnav_parser.c)

The curve-fit check compared two second-of-week values and never read a
week at all, so a record exactly N weeks old read as current. Harmless
while ephemeris lived in RAM; directly reachable once persisted across a
power-off. gps_resolve_toe_week() now reconstructs Toe's week at commit
time from the subframe 1 week number and the handover word; an
unresolvable week yields no age, which reads as a rejection.

A restored record could also outrank the live broadcast, because the Toe
ordering inverts past half a week. Flash records are now marked restored
and are candidates, never incumbents.

Flash wear (gps_nav_context_store.c, gps_interrupt_handlers.c)

The CRC wear policy never engaged: the payload carried the live WLS
position and a receiver timestamp nothing on the restore path read, so
every 60 s save was an erase - about 139 days to wear-out. The seed is
now held and refreshed only on 5 km of movement, at most hourly: one
erase per day instead of 1440.

Measurement model (NavEquations.c)

The elevation weighting was floored at 10 degrees, capping its term at
17.3 m where the model's own answer is 34.4 m at 5 degrees and 86 m at 2,
so a low satellite was trusted several times more than the model allowed.
Floor lowered to 3 degrees, which is where 1/sin must still be bounded
because a zero weight fails the whole epoch. Measured on a six-satellite
geometry, 30 m of uncorrected delay now moves the fix 4.18 m through a
4-degree satellite against 48.50 m through a 72-degree one.

Both atmospheric corrections switched off below the horizon, so a
satellite crossing it lost 78.8 m of tropospheric delay in one step.
Elevation is now clamped at the horizon for the models, and the
extrapolation is paid for in variance.

Also: gps_pps_select_reference() returned early on the preferred PRN,
leaving later non-finite capture times unscreened.

Checked and found correct, with regressions added: the epoch assembly
under satellite connect/disconnect and reassignment, coalesced captures,
the 32-bit RF sample counter wrap (pseudoranges agree to 3.3 um across
it), the refusal to publish a solvable subset of an incoherent epoch, and
the Klobuchar model against IS-GPS-200 20.3.3.5.2.5.

Tests: 22 host suites, 5 RTL testbenches, sanitizer clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 01:45:35 +03:00

169 lines
6.5 KiB
C

#include <stdio.h>
#include <string.h>
#include "gps/gps_ip_defines.h"
static int failures;
#define CHECK(c,m) do{ if(!(c)){ fprintf(stderr,"FAIL: %s\n",m); failures++; } }while(0)
int main(void)
{
unsigned int sf[10];
unsigned char iode = 0u;
/* Subframe 1: IODC is split, two MSBs in word 3 bits 7..6, eight LSBs in
* word 8 bits 29..22. The low byte must equal the IODE. */
memset(sf, 0, sizeof(sf));
sf[2] = (0x02u << 6); /* IODC[9:8] = 2 */
sf[7] = (0x5Au << 22); /* IODC[7:0] = 0x5A */
CHECK(gps_lnav_broadcast_iode(sf, 1u, &iode), "sf1 iode not extracted");
CHECK(iode == 0x5Au, "sf1 iode wrong");
/* Subframe 2: IODE in word 3 bits 29..22. */
memset(sf, 0, sizeof(sf));
sf[2] = (0xA3u << 22);
CHECK(gps_lnav_broadcast_iode(sf, 2u, &iode), "sf2 iode not extracted");
CHECK(iode == 0xA3u, "sf2 iode wrong");
/* Subframe 3: IODE in word 10 bits 29..22. */
memset(sf, 0, sizeof(sf));
sf[9] = (0x17u << 22);
CHECK(gps_lnav_broadcast_iode(sf, 3u, &iode), "sf3 iode not extracted");
CHECK(iode == 0x17u, "sf3 iode wrong");
/* Subframes 4 and 5 carry no issue of data. */
CHECK(!gps_lnav_broadcast_iode(sf, 4u, &iode), "sf4 reported an iode");
CHECK(!gps_lnav_broadcast_iode(sf, 5u, &iode), "sf5 reported an iode");
CHECK(!gps_lnav_broadcast_iode(NULL, 2u, &iode), "null subframe accepted");
CHECK(!gps_lnav_broadcast_iode(sf, 2u, NULL), "null output accepted");
/* All three subframes of one data set must agree, which is what the
* cross-check relies on. */
unsigned int set1[10], set2[10], set3[10];
memset(set1,0,sizeof(set1)); memset(set2,0,sizeof(set2)); memset(set3,0,sizeof(set3));
set1[2] = (0x01u << 6); set1[7] = (0x77u << 22);
set2[2] = (0x77u << 22);
set3[9] = (0x77u << 22);
unsigned char a=0,b=0,c=0;
gps_lnav_broadcast_iode(set1,1u,&a);
gps_lnav_broadcast_iode(set2,2u,&b);
gps_lnav_broadcast_iode(set3,3u,&c);
CHECK((a==b)&&(b==c)&&(a==0x77u), "one data set gave inconsistent iode");
/*
* Restoring a persisted page must repopulate the raw-word cache, not just
* the parsed model. The periodic save rebuilds its payload from that
* cache, so a restore that only set the parsed form would write the page
* back as absent and wipe the stored model on the first save after every
* reboot. That regression reached hardware once and was caught only by
* watching "iono=0" appear in the save telemetry.
*/
unsigned int page18[10];
memset(page18, 0, sizeof(page18));
/* data ID 1, SV ID 56 -> subframe 4 page 18, with non-zero coefficients. */
page18[2] = (1u << 28) | (56u << 22) | (0x1au << 14) | (0x0bu << 6);
page18[3] = (0x21u << 22) | (0x33u << 14) | (0x44u << 6);
page18[4] = (0x55u << 22) | (0x66u << 14) | (0x77u << 6);
page18[5] = (0x12u << 22) | (0x34u << 14) | (0x56u << 6);
uint32_t words[10];
CHECK(!gps_lnav_get_iono_page(words),
"iono page reported before anything was stored");
CHECK(gps_lnav_restore_iono_page(page18), "restoring page 18 failed");
CHECK(gps_lnav_get_iono_page(words),
"restored iono page did not repopulate the raw-word cache");
for(unsigned int i = 0u; i < 10u; i++)
{
CHECK(words[i] == (uint32_t)page18[i],
"restored iono raw words differ from what was restored");
}
/* Same contract for almanac pages. */
unsigned int alm[10];
memset(alm, 0, sizeof(alm));
alm[2] = (1u << 28) | (5u << 22) | (0x0f3eu << 6); /* SV ID 5 */
alm[3] = (36u << 22) | (2439u << 6);
alm[4] = (0xfd50u << 14);
alm[5] = (10554475u << 6);
uint32_t pages[GPS_ALMANAC_PRN_COUNT][10];
CHECK(gps_lnav_restore_almanac_page(alm, 5u),
"restoring an almanac page failed");
uint32_t mask = gps_lnav_get_almanac_pages(pages);
CHECK((mask & (1u << 4)) != 0u,
"restored almanac page not present in the raw-word cache");
for(unsigned int i = 0u; i < 10u; i++)
{
CHECK(pages[4][i] == (uint32_t)alm[i],
"restored almanac raw words differ from what was restored");
}
/*
* Toe carries no week on the air. The decoder reconstructs it from the
* subframe 1 week number and the handover word of the subframe that
* carried Toe, which is the only self-consistent pair available: Toe is
* within a couple of hours of its own transmission, so the week that puts
* it closest to that transmission is the right one.
*
* Without this the curve-fit age check works modulo one week and a record
* exactly N weeks old reads as current - harmless while ephemeris lives in
* RAM, reachable the moment it is persisted across a power-off.
*/
{
struct { const char* name; unsigned int wn; double how_tow;
double toe_sec; int expect_week; } cases[] = {
/* HOW TOW is a count of six-second units and Toe is a 16-bit
* field scaled by 16, so both are quantised on the air. */
{"mid-week", 430u, 172800.0, 172800.0, 430},
{"toe two hours ahead", 430u, 172800.0, 180000.0, 430},
/* Broadcast at the end of the week, Toe early in the next one. */
{"toe crosses forward", 430u, 604194.0, 1808.0, 431},
/* Broadcast just after the rollover, Toe late in the previous. */
{"toe crosses back", 431u, 1806.0, 604192.0, 430},
};
for(unsigned int c = 0u; c < 4u; c++)
{
gps_frame_t frame;
unsigned int s1[10], s2[10], s3[10];
memset(&frame, 0, sizeof(frame));
memset(s1, 0, sizeof(s1));
memset(s2, 0, sizeof(s2));
memset(s3, 0, sizeof(s3));
/* HOW: TOW count in six-second units, then the subframe id. */
unsigned int how = (unsigned int)(cases[c].how_tow / 6.0);
s1[1] = (how << 13) | (1u << 8);
s2[1] = (how << 13) | (2u << 8);
s3[1] = (how << 13) | (3u << 8);
/* Subframe 1: week number and the IODC whose low byte is the IODE. */
s1[2] = (cases[c].wn << 20);
s1[7] = (0x64u << 22);
/* Subframes 2 and 3 carry the matching IODE, and 2 carries Toe. */
s2[2] = (0x64u << 22);
s2[9] = (((unsigned int)(cases[c].toe_sec / 16.0)) << 14);
s3[9] = (0x64u << 22);
parse_subframe(s1, &frame, NULL);
parse_subframe(s2, &frame, NULL);
parse_subframe(s3, &frame, NULL);
CHECK(frame.gps_ephems.ephemeris_valid,
"consistent ephemeris set was not committed");
CHECK(frame.gps_ephems.toe_week_valid,
"toe week was not resolved");
if(frame.gps_ephems.toe.week != cases[c].expect_week)
{
fprintf(stderr, "FAIL: %s: toe week %d, expected %d\n",
cases[c].name, frame.gps_ephems.toe.week,
cases[c].expect_week);
failures++;
}
CHECK(frame.gps_ephems.toe.sec == cases[c].toe_sec,
"toe seconds did not survive the round trip");
}
}
if(failures==0) puts("gps lnav iode tests passed");
return failures==0?0:1;
}