fix(gps): make ephemeris flash persistence hardware-safe

Add freestanding S25FL256S read/program/erase/verify support and seed a newly formatted ephemeris store with stale records. Age-gate navigation readiness, persist the continuous Toe week, and regenerate the Genesys2 MAX2771 integration and flash image.
This commit is contained in:
onur
2026-08-13 00:50:57 +03:00
parent 97e3a71ab9
commit a25a1a61d0
43 changed files with 722126 additions and 713769 deletions
@@ -17,7 +17,7 @@ typedef struct {
uint16_t record_len;
uint32_t sequence;
uint32_t toe_sec;
uint16_t week;
uint16_t toe_week;
uint16_t iodc;
uint8_t prn;
uint8_t iode;
@@ -385,6 +385,8 @@ bool gps_ephemeris_store_save(
if((store == NULL) || !store->mounted || store->capacity_exceeded ||
(ephemeris == NULL) ||
(subframe_words == NULL) || (prn >= GPS_PRN_COUNT) ||
!ephemeris->toe_week_valid || (ephemeris->toe.week < 0) ||
(ephemeris->toe.week > 0xffff) ||
(gps_ephemeris_check_static(ephemeris, prn) != GPS_EPHEMERIS_OK))
{
return false;
@@ -426,7 +428,7 @@ bool gps_ephemeris_store_save(
record.record_len = (uint16_t)sizeof(record);
record.sequence = store->sequence + 1u;
record.toe_sec = (uint32_t)ephemeris->toe.sec;
record.week = (uint16_t)ephemeris->WN;
record.toe_week = (uint16_t)ephemeris->toe.week;
record.iodc = (uint16_t)ephemeris->iodc;
record.prn = (uint8_t)prn;
record.iode = ephemeris->iode;
@@ -508,11 +510,23 @@ unsigned int gps_ephemeris_store_load_all(
break;
}
}
if(!parsed || !frame.gps_ephems.ephemeris_valid)
if(!parsed || !frame.gps_ephems.ephemeris_valid ||
(record.toe_sec != (uint32_t)frame.gps_ephems.toe.sec) ||
(record.iodc != (uint16_t)frame.gps_ephems.iodc) ||
(record.iode != frame.gps_ephems.iode) ||
(record.health != frame.gps_ephems.svhlth) ||
(record.flags != (frame.gps_ephems.fit_flag ? 1u : 0u)) ||
((record.toe_week & 0x03ffu) !=
((uint16_t)frame.gps_ephems.toe.week & 0x03ffu)))
{
store->records_rejected++;
break;
}
/* Raw LNAV only carries WN modulo 1024. Version 2 records retain
* the continuous week resolved from HOW when the live set was
* committed, so flash restore preserves the age domain. */
frame.gps_ephems.toe.week = (int)record.toe_week;
frame.gps_ephems.toe_week_valid = true;
frame.gps_ephems.SVID = (unsigned char)prn;
if(gps_ephemeris_check_static(&frame.gps_ephems, prn) !=
GPS_EPHEMERIS_OK)
@@ -527,3 +541,89 @@ unsigned int gps_ephemeris_store_load_all(
}
return delivered;
}
static void build_initial_seed_subframes(
unsigned int words[3][10], unsigned int iodc)
{
/*
* Week 380 and Toe Tuesday 00:00 are intentionally old. Keep the orbital
* fields structurally plausible so restore exercises the same decoder and
* validity rules as a live LNAV data set, while the age check guarantees
* that this diagnostic seed can never be used as current ephemeris.
*/
const unsigned int week = 380u;
const unsigned int toe_sec = 86400u;
const unsigned int how_count = toe_sec / 6u;
const unsigned int iode = iodc & 0xffu;
const uint32_t ecc_raw = (uint32_t)(0.008 * 8589934592.0);
const uint32_t sqrta_raw = (uint32_t)(5153.6 * 524288.0);
memset(words, 0, sizeof(unsigned int) * 3u * 10u);
for(unsigned int sf = 0u; sf < 3u; sf++)
{
words[sf][1] = ((how_count + sf) << 13) | ((sf + 1u) << 8);
}
words[0][2] = (week << 20) | (2u << 14) |
(((iodc >> 8) & 0x03u) << 6);
words[0][7] = ((iodc & 0xffu) << 22) | ((toe_sec >> 4) << 6);
words[1][2] = iode << 22;
words[1][5] = ((ecc_raw >> 24) & 0xffu) << 6;
words[1][6] = (ecc_raw & 0xffffffu) << 6;
words[1][7] = ((sqrta_raw >> 24) & 0xffu) << 6;
words[1][8] = (sqrta_raw & 0xffffffu) << 6;
words[1][9] = (toe_sec >> 4) << 14;
words[2][9] = iode << 22;
}
unsigned int gps_ephemeris_store_seed_initial(
gps_ephemeris_store_t* store)
{
if((store == NULL) || !store->mounted || (store->generation == 0u))
{
return 0u;
}
unsigned int seeded = 0u;
for(unsigned int prn = 0u;
prn < GPS_EPH_STORE_INITIAL_SEED_COUNT;
prn++)
{
unsigned int words[3][10];
build_initial_seed_subframes(words, 0x120u + prn);
gps_frame_t frame;
memset(&frame, 0, sizeof(frame));
frame.gps_ephems.SVID = (unsigned char)prn;
bool parsed = true;
for(unsigned int sf = 0u; sf < 3u; sf++)
{
unsigned int how_tow = 0u;
if(parse_subframe(words[sf], &frame, &how_tow) !=
(unsigned char)(sf + 1u))
{
parsed = false;
break;
}
}
frame.gps_ephems.SVID = (unsigned char)prn;
if(!parsed || !frame.gps_ephems.ephemeris_valid ||
(gps_ephemeris_check_static(&frame.gps_ephems, prn) !=
GPS_EPHEMERIS_OK))
{
break;
}
bool wrote = false;
if(!gps_ephemeris_store_save(
store, prn, &frame.gps_ephems,
(const unsigned int (*)[10])words, &wrote) || !wrote)
{
break;
}
seeded++;
}
return seeded;
}
@@ -363,6 +363,40 @@ static void gps_nav_epoch_account(uint32_t expected_mask,
}
}
static bool gps_nav_channel_ephemeris_ready(unsigned int tracking_index)
{
if(tracking_index >= GPS_TRCK_IP_COUNT)
{
return false;
}
volatile gps_ephem_ll_t* node =
get_element_by_ip_number(&tracking_node,
(unsigned char)tracking_index);
if((node == NULL) || (node->data_p == NULL) ||
!node->gps_how_anchor_valid)
{
return false;
}
int time_week;
double time_sec;
if(node->gps_nav_solve_sequence != 0u)
{
time_week = node->gps_last_nav_solve_tow.week;
time_sec = node->gps_last_nav_solve_tow.sec;
}
else
{
time_week = node->gps_how_tow.week;
time_sec = node->gps_how_tow.sec;
}
unsigned int prn = trck_ips[tracking_index]->PRN_NUMBER;
return gps_ephemeris_check(
(const gps_ephemeris_t*)node->data_p, prn, true,
time_week, time_sec) == GPS_EPHEMERIS_OK;
}
static uint32_t gps_nav_ready_channel_mask(void)
{
uint32_t eligible_mask = 0u;
@@ -380,7 +414,7 @@ static uint32_t gps_nav_ready_channel_mask(void)
* an unproven seed cannot hold up a coalescing epoch; it joins as soon
* as its first epoch validates. */
if(!tracking_allocated[i] || !busy || !locked || !nav_irq_enabled ||
trk_seed_unconfirmed[i])
trk_seed_unconfirmed[i] || !gps_nav_channel_ephemeris_ready(i))
{
continue;
}
@@ -408,7 +442,7 @@ static uint32_t gps_nav_ready_channel_mask(void)
return eligible_mask;
}
#if (GPS_EPHEMERIS_FLASH_ENABLE == 1)
#if ((GPS_EPHEMERIS_FLASH_ENABLE == 1) || (GPS_NAV_CONTEXT_ENABLE == 1))
static void gps_flash_read(void* ctx, uint32_t address, void* dst,
uint32_t len)
{
@@ -429,6 +463,28 @@ static bool gps_flash_program(void* ctx, uint32_t address, const void* src,
return bsp_spi_flash_program(address, src, len);
}
static bool gps_flash_device_init(void)
{
bsp_spi_flash_init();
bsp_spi_flash_reset();
uint32_t id = bsp_spi_flash_read_id();
uint8_t status = bsp_spi_flash_read_status();
uint8_t config = bsp_spi_flash_read_config();
GPS_LOG(DEBUG_STATUS, GPS_LOG_CAT_NAV,
"SPI flash: id=0x%06lx status=0x%02x config=0x%02x\r\n",
(unsigned long)id, status, config);
if(id != BSP_SPI_FLASH_JEDEC_ID)
{
GPS_LOG(DEBUG_ERROR, GPS_LOG_CAT_NAV,
"unsupported SPI flash JEDEC id=0x%06lx expected=0x%06lx\r\n",
(unsigned long)id,
(unsigned long)BSP_SPI_FLASH_JEDEC_ID);
return false;
}
return true;
}
#if (GPS_EPHEMERIS_FLASH_ENABLE == 1)
static void gps_ephemeris_flash_loaded(void* ctx, unsigned int prn,
const gps_ephemeris_t* ephemeris)
{
@@ -450,12 +506,14 @@ static void gps_ephemeris_flash_loaded(void* ctx, unsigned int prn,
prn, ephemeris->iode, ephemeris->toe.sec);
}
}
#endif
/*
* Persistent navigation context: almanac, ionosphere model, last position.
* Kept separate from the ephemeris store because it has a different lifetime
* (weeks, not hours) and a different shape (one small blob, not a per-PRN log).
*/
#if (GPS_NAV_CONTEXT_ENABLE == 1)
static bool gps_receiver_time_now(int* week_out, double* tow_sec_out);
static gps_nav_context_store_t gps_nav_context_store;
@@ -696,6 +754,9 @@ static void gps_nav_context_flash_service(void)
}
}
#endif /* GPS_NAV_CONTEXT_ENABLE */
#if (GPS_EPHEMERIS_FLASH_ENABLE == 1)
static void gps_ephemeris_flash_init(void)
{
gps_ephemeris_store_flash_t flash;
@@ -716,14 +777,18 @@ static void gps_ephemeris_flash_init(void)
"persistent ephemeris store unavailable\r\n");
return;
}
if((bsp_spi_flash_read_config() & BSP_SPI_FLASH_CONFIG_TBPARM) != 0u)
{
gps_ephemeris_store_ready = false;
gps_runtime_diagnostics.flash_ephemeris_error_total++;
GPS_LOG(DEBUG_ERROR, GPS_LOG_CAT_NAV,
"ephemeris flash disabled: CR1.TBPARM places 4 KiB sectors at the top\r\n");
return;
}
#if (GPS_EPHEMERIS_STORE_MAX_SLOTS != 0u)
gps_ephemeris_store_limit_slots(&gps_ephemeris_store,
GPS_EPHEMERIS_STORE_MAX_SLOTS);
#endif
unsigned int loaded = gps_ephemeris_store_load_all(
&gps_ephemeris_store, gps_ephemeris_flash_loaded, NULL);
gps_runtime_diagnostics.flash_ephemeris_loaded_total = loaded;
/*
* A blank or unreadable store needs two sector erases before it can hold
* anything. Spend them here, during init, rather than letting the first
@@ -744,8 +809,35 @@ static void gps_ephemeris_flash_init(void)
gps_runtime_diagnostics.flash_ephemeris_error_total++;
GPS_LOG(DEBUG_WARNING, GPS_LOG_CAT_NAV,
"persistent ephemeris store format failed; persistence disabled\r\n");
return;
}
}
unsigned int loaded = gps_ephemeris_store_load_all(
&gps_ephemeris_store, gps_ephemeris_flash_loaded, NULL);
#if (GPS_EPHEMERIS_FLASH_SEED_INITIAL == 1)
if(loaded == 0u)
{
unsigned int seeded =
gps_ephemeris_store_seed_initial(&gps_ephemeris_store);
if(seeded != GPS_EPH_STORE_INITIAL_SEED_COUNT)
{
gps_runtime_diagnostics.flash_ephemeris_error_total++;
GPS_LOG(DEBUG_WARNING, GPS_LOG_CAT_NAV,
"initial stale ephemeris seed incomplete: wrote=%u expected=%u\r\n",
seeded, GPS_EPH_STORE_INITIAL_SEED_COUNT);
}
else
{
GPS_LOG(DEBUG_STATUS, GPS_LOG_CAT_NAV,
"initial stale ephemeris seed written: records=%u\r\n",
seeded);
}
loaded = gps_ephemeris_store_load_all(
&gps_ephemeris_store, gps_ephemeris_flash_loaded, NULL);
}
#endif
gps_runtime_diagnostics.flash_ephemeris_loaded_total = loaded;
GPS_LOG(DEBUG_INFO, GPS_LOG_CAT_NAV,
"persistent ephemeris store mounted: generation=%u loaded=%u rejected=%u\r\n",
gps_ephemeris_store.generation, loaded,
@@ -831,6 +923,7 @@ static void gps_ephemeris_flash_queue(unsigned int prn,
gps_ephemeris_flash_pending_mask |= (1u << prn);
}
#endif /* GPS_EPHEMERIS_FLASH_ENABLE */
#endif /* GPS_EPHEMERIS_FLASH_ENABLE || GPS_NAV_CONTEXT_ENABLE */
static void gps_prn_context_store(unsigned int prn,
int week,
@@ -1065,6 +1158,7 @@ static void gps_refresh_acq_visibility(void)
* retried slowly, and it self-corrects the moment a real fix replaces
* the seed.
*/
#if (GPS_NAV_CONTEXT_ENABLE == 1)
if(!gps_nav_context_position_seed_valid)
{
gps_acq_manager_set_search_mask(&gps_acq_manager, 0xffffffffu);
@@ -1086,6 +1180,10 @@ static void gps_refresh_acq_visibility(void)
* back to its normal value.
*/
mask_elevation_deg = GPS_ACQ_VISIBILITY_SEED_MASK_ELEVATION_DEG;
#else
gps_acq_manager_set_search_mask(&gps_acq_manager, 0xffffffffu);
return;
#endif
}
uint32_t mask = 0xffffffffu;
@@ -1293,10 +1391,19 @@ static void gps_runtime_state_init(void)
gps_ephemeris_cache_init(
&gps_ephemeris_cache,
GPS_EPHEMERIS_CACHE_MAX_AGE_TICKS);
#if ((GPS_EPHEMERIS_FLASH_ENABLE == 1) || (GPS_NAV_CONTEXT_ENABLE == 1))
bool gps_flash_ready = gps_flash_device_init();
#if (GPS_EPHEMERIS_FLASH_ENABLE == 1)
gps_ephemeris_flash_init();
if(gps_flash_ready)
{
gps_ephemeris_flash_init();
}
#endif
#if (GPS_NAV_CONTEXT_ENABLE == 1)
gps_nav_context_flash_init();
if(gps_flash_ready)
{
gps_nav_context_flash_init();
}
#endif
#endif
@@ -2147,32 +2254,10 @@ static void gps_process_tracking_nav_bits(const gps_isr_event_t* event)
unsigned char subframe_id = parse_subframe((unsigned int*)gps_subframe,
(gps_frame_t*)gps_frame_parsed,
&how_tow_sec);
if((subframe_id >= 1u) && (subframe_id <= 3u) &&
(gps_frame_parsed->pending_ephemeris_mask == 0u))
{
bool ephemeris_changed = false;
if(gps_ephemeris_cache_store_if_new(
&gps_ephemeris_cache, event->prn,
(const gps_ephemeris_t*)&gps_frame_parsed->gps_ephems,
event->tick, &ephemeris_changed) &&
ephemeris_changed)
{
gps_runtime_diagnostics.ephemeris_cache_store_total++;
#if (GPS_EPHEMERIS_FLASH_ENABLE == 1)
/* Only a genuinely new data set reaches flash; the same
* broadcast decoded again is filtered out above. */
gps_ephemeris_flash_queue(
event->prn,
(const unsigned int (*)[10])
gps_frame_parsed->pending_ephemeris_words);
#endif
GPS_LOG(DEBUG_INFO, GPS_LOG_CAT_NAV,
"stored ephemeris cache: prn=%u trk=%u iode=%u toe=%.0f\r\n",
event->prn, trck_ip_no,
gps_frame_parsed->gps_ephems.iode,
gps_frame_parsed->gps_ephems.toe.sec);
}
}
bool ephemeris_committed =
(subframe_id >= 1u) && (subframe_id <= 3u) &&
(gps_frame_parsed->pending_ephemeris_mask == 0u);
bool how_anchor_accepted = false;
if((subframe_id >= 1u) && (subframe_id <= 5u) &&
(how_tow_sec < (unsigned int)GPS_TIME_WEEK_SECONDS))
{
@@ -2226,6 +2311,19 @@ static void gps_process_tracking_nav_bits(const gps_isr_event_t* event)
(double)how_tow_sec, event,
node->gps_how_week_valid ||
(subframe_id == 1u));
/* The decoder can only reconstruct Toe in the broadcast's
* modulo-1024 week domain. Once HOW has supplied the receiver's
* continuous week, lift Toe into that same domain before the
* curve-fit age check compares them. */
if(gps_frame_parsed->gps_ephems.toe_week_valid)
{
gps_frame_parsed->gps_ephems.toe.week =
gps_time_extend_lnav_week(
true, anchor_week,
(unsigned int)gps_frame_parsed->gps_ephems.toe.week);
}
gps_frame_parsed->gps_ephems.tow.week = anchor_week;
how_anchor_accepted = true;
GPS_LOG(recovered_anchor ? DEBUG_WARNING : DEBUG_TRACE,
GPS_LOG_CAT_NAV,
"%s HOW anchor: trk=%u prn=%u week=%d tow=%.3f abs=%u corrected=%.6f\r\n",
@@ -2258,6 +2356,36 @@ static void gps_process_tracking_nav_bits(const gps_isr_event_t* event)
trck_ip_no, subframe_id, how_tow_sec);
}
/* Cache only after Toe has been lifted from WN modulo 1024 into the HOW
* anchor's continuous week. Flash persists that full week; storing before
* this point makes a fresh record appear 1024 or 2048 weeks old at boot. */
if(ephemeris_committed && how_anchor_accepted)
{
bool ephemeris_changed = false;
if(gps_ephemeris_cache_store_if_new(
&gps_ephemeris_cache, event->prn,
(const gps_ephemeris_t*)&gps_frame_parsed->gps_ephems,
event->tick, &ephemeris_changed) &&
ephemeris_changed)
{
gps_runtime_diagnostics.ephemeris_cache_store_total++;
#if (GPS_EPHEMERIS_FLASH_ENABLE == 1)
/* Only a genuinely new data set reaches flash; the same
* broadcast decoded again is filtered out above. */
gps_ephemeris_flash_queue(
event->prn,
(const unsigned int (*)[10])
gps_frame_parsed->pending_ephemeris_words);
#endif
GPS_LOG(DEBUG_INFO, GPS_LOG_CAT_NAV,
"stored ephemeris cache: prn=%u trk=%u iode=%u toe=%d/%.0f\r\n",
event->prn, trck_ip_no,
gps_frame_parsed->gps_ephems.iode,
gps_frame_parsed->gps_ephems.toe.week,
gps_frame_parsed->gps_ephems.toe.sec);
}
}
gps_mark_node_fresh(node, event->status, event->tick);
GPS_LOG(DEBUG_TRACE, GPS_LOG_CAT_TRK, "nav bits: trk=%u prn=%u mask=0x%x status=0x%x\r\n",
+19 -10
View File
@@ -171,14 +171,6 @@ static bool gps_node_has_committed_ephemeris(volatile gps_ephem_ll_t* node)
node->data_p->ephemeris_valid;
}
static bool gps_ephemeris_ready_for_position(
const gps_ephemeris_t* eph,
unsigned int expected_svid)
{
return gps_ephemeris_check_static(eph, expected_svid) ==
GPS_EPHEMERIS_OK;
}
/*
* Same structural rules plus the IS-GPS-200 curve fit window evaluated at the
* epoch's own transmit time. This is what keeps a cached or flash-restored
@@ -229,9 +221,26 @@ static bool gps_node_ready_for_position(volatile gps_ephem_ll_t* node)
return false;
}
return gps_ephemeris_ready_for_position(
if(!node->gps_how_anchor_valid)
{
return false;
}
int time_week;
double time_sec;
if(node->gps_nav_solve_sequence != 0u)
{
time_week = node->gps_last_nav_solve_tow.week;
time_sec = node->gps_last_nav_solve_tow.sec;
}
else
{
time_week = node->gps_how_tow.week;
time_sec = node->gps_how_tow.sec;
}
return gps_ephemeris_check(
(const gps_ephemeris_t*)node->data_p,
trck_ips[node->tracking_ip_no]->PRN_NUMBER);
trck_ips[node->tracking_ip_no]->PRN_NUMBER,
true, time_week, time_sec) == GPS_EPHEMERIS_OK;
}
static bool gps_channel_nav_solve_enabled(unsigned char trck_ip_no)