From 93207022dd78f89de56fc143f9863e6598941b68 Mon Sep 17 00:00:00 2001 From: timohueser Date: Sat, 12 Sep 2026 12:14:27 +0200 Subject: [PATCH 1/3] fix: configure late GPS responses during startup --- firmware/obc-fw-nrf54l/README.md | 8 +++++ firmware/obc-fw-nrf54l/src/ride.rs | 5 ++-- firmware/obc-fw-nrf54l/src/sensors.rs | 40 ++++++++++++++++++------- firmware/obc-platform/src/sensor_hub.rs | 6 ++-- 4 files changed, 43 insertions(+), 16 deletions(-) diff --git a/firmware/obc-fw-nrf54l/README.md b/firmware/obc-fw-nrf54l/README.md index dd18e14ce..9374e402d 100644 --- a/firmware/obc-fw-nrf54l/README.md +++ b/firmware/obc-fw-nrf54l/README.md @@ -182,6 +182,14 @@ nothing to wire on the GPS-21834. the receiver's RTC + ephemeris across a power-off, turning every cold ~30 s fix into a hot/warm fix in seconds — the biggest UX win for a device switched off at each stop. +If GPS does not answer the first startup probe, the sensor task retries at the normal DDC poll +interval during the existing 150-second boot acquisition window. On the first response, it sends +the GPS configuration before it reads fixes. The startup sensor-warning bundle is published then, +or at the deadline if GPS remains absent. This can delay the altimeter and compass warnings too; +their original probe results are retained. A successful first GPS probe publishes immediately. +This is a one-time startup result, not a report of live sensor availability. Intentional idle +backup sleep still produces no DDC polling. + ## Build & flash **One-time prerequisite (#617): flash the bootloader.** The app is linked at `0x8000` — diff --git a/firmware/obc-fw-nrf54l/src/ride.rs b/firmware/obc-fw-nrf54l/src/ride.rs index 762dc5121..5500ce419 100644 --- a/firmware/obc-fw-nrf54l/src/ride.rs +++ b/firmware/obc-fw-nrf54l/src/ride.rs @@ -1112,9 +1112,8 @@ pub(crate) async fn run_app( } // ── Sensor presence → warning (issue #504), real-sensor build, once ── - // The sensor task publishes its boot I²C probe result a moment after boot; map any chip that - // didn't answer to a dismissable warning card. `try_take` yields once, so this fires a single - // pass; an empty flag set is a no-op. + // The sensor task publishes once GPS responds or its startup deadline passes. Map chips + // absent at that point to a dismissable warning; this is not a live-availability stream. #[cfg(all(not(feature = "debug-uart"), not(feature = "synth")))] if let Some(p) = consumer.take_presence() { let mut w = obc_app::WarningFlags::NONE; diff --git a/firmware/obc-fw-nrf54l/src/sensors.rs b/firmware/obc-fw-nrf54l/src/sensors.rs index d822a7758..35299fb0c 100644 --- a/firmware/obc-fw-nrf54l/src/sensors.rs +++ b/firmware/obc-fw-nrf54l/src/sensors.rs @@ -158,7 +158,7 @@ pub async fn sensor_task(mut twim: Twim<'static>, mut txready: Input<'static>, l // --- Boot probe: loud RTT so a wiring/power fault is obvious before anything else. --- let baro_addr = probe_bmp581(&mut twim).await; let icm_addr = probe_icm20948(&mut twim).await; - let gps_ok = probe_m10(&mut twim).await; + let mut gps_ok = probe_m10(&mut twim).await; if let Some(addr) = baro_addr { configure_bmp581(&mut twim, addr).await; @@ -169,17 +169,19 @@ pub async fn sensor_task(mut twim: Twim<'static>, mut txready: Input<'static>, l if gps_ok { configure_m10(&mut twim, DEFAULT_INTERVAL_S).await; } else { - warn!("sensors: GPS not answering — the loop will keep polling so a late-powered module is picked up"); + warn!("sensors: GPS not answering — retrying during boot acquisition"); } // Whether the compass is live — the AK09916 magnetometer is read at AK_ADDR through the ICM's // bypass, so only its *presence* (a successful ICM probe + config) matters at read time. let compass_ok = icm_addr.is_some(); - // Surface the probe result on glass (issue #504): any chip that didn't answer becomes a - // dismissable warning the ride loop raises. Published once — a missing module is a wiring/power - // fault, not a transient. (A missing GPS *module* is distinct from "no fix yet".) - link.dispatch_presence(SensorPresence { gps: gps_ok, altimeter: baro_addr.is_some(), compass: compass_ok }); + // The warning bundle is published once: immediately when GPS responds, otherwise after the + // bounded startup window. A receiver still starting must not leave a stale missing-GPS warning. + let mut presence = SensorPresence { gps: gps_ok, altimeter: baro_addr.is_some(), compass: compass_ok }; + if gps_ok { + link.dispatch_presence(presence); + } let mut acc = [0u8; ACC_CAP]; let mut acc_len = 0usize; @@ -192,8 +194,23 @@ pub async fn sensor_task(mut twim: Twim<'static>, mut txready: Input<'static>, l info!("sensors: boot acquisition — holding awake for the first fix (≤ {=u64}s)", BOOT_ACQUIRE_TIMEOUT_S); let boot_deadline = Instant::now() + Duration::from_secs(BOOT_ACQUIRE_TIMEOUT_S); loop { - wait_data_event(&mut txready, interval_s, &mut st).await; - if drain_and_publish(&mut twim, &mut acc, &mut acc_len, baro_addr, &mut st, link).await { + if gps_ok { + wait_data_event(&mut txready, interval_s, &mut st).await; + } else { + // An absent receiver cannot supply a useful TX-Ready edge. Keep its probes at the + // normal poll cadence even if that unconnected input is noisy. + Timer::at((Instant::now() + poll_deadline(interval_s)).min(boot_deadline)).await; + if Instant::now() >= boot_deadline { + break; + } + gps_ok = probe_m10(&mut twim).await; + if gps_ok { + configure_m10(&mut twim, interval_s).await; + presence.gps = true; + link.dispatch_presence(presence); + } + } + if gps_ok && drain_and_publish(&mut twim, &mut acc, &mut acc_len, baro_addr, &mut st, link).await { break; // got the boot fix } if Instant::now() >= boot_deadline { @@ -204,6 +221,10 @@ pub async fn sensor_task(mut twim: Twim<'static>, mut txready: Input<'static>, l break; } } + if !gps_ok { + error!("sensors: GPS did not answer during boot acquisition — check wiring / power"); + link.dispatch_presence(presence); + } // --- Phase 2: power-managed steady state. Honour the app's requested GpsPower — deep-sleep when // idle, full / low-power fixes while riding — and keep streaming fixes. --- @@ -479,14 +500,13 @@ async fn probe_bmp581(twim: &mut Twim<'static>) -> Option { None } -/// Probe the SAM-M10Q by reading its DDC byte-count register; log whether it answers. +/// Probe the SAM-M10Q by reading its DDC byte-count register. Absence is reported at the deadline. async fn probe_m10(twim: &mut Twim<'static>) -> bool { let mut cnt = [0u8; 2]; if twim.write_read(M10_ADDR, &[DDC_COUNT_REG], &mut cnt).await.is_ok() { info!("SAM-M10Q alive @ {=u8:#04x} ({=u16} DDC bytes pending)", M10_ADDR, u16::from_be_bytes(cnt)); true } else { - error!("SAM-M10Q no ACK on DDC {=u8:#04x} — check Qwiic wiring / 3V3 / V_BCKP", M10_ADDR); false } } diff --git a/firmware/obc-platform/src/sensor_hub.rs b/firmware/obc-platform/src/sensor_hub.rs index b1fcfcc3c..98ac52a2b 100644 --- a/firmware/obc-platform/src/sensor_hub.rs +++ b/firmware/obc-platform/src/sensor_hub.rs @@ -56,7 +56,7 @@ use obc_ports::{ /// run on different executors / priorities on the board. type Sig = Signal; -/// Which sensors answered the boot I²C probe — the sensor task's probe results, carried to the app +/// Which sensors answered during startup — the sensor task's results, carried to the app /// so a missing module surfaces as a dismissable warning rather than only an RTT line. A missing /// GPS is distinct from "no fix yet" (the receiver is there, just no sky): this is the *module*. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -262,7 +262,7 @@ impl SensorTaskLink<'_> { self.0.publish(&self.0.heading, deg); } - /// Publish the boot probe result (once, after the sensor task probes all three chips). Pulses the + /// Publish the startup result once, after GPS responds or its acquisition deadline passes. Pulses the /// event so the ride loop wakes and drains it via [`SensorConsumer::take_presence`]. pub fn dispatch_presence(&self, p: SensorPresence) { self.0.publish(&self.0.presence, p); @@ -380,7 +380,7 @@ impl<'a> SensorConsumer<'a> { SensorCadence(&self.0.cadence) } - /// Drain the boot probe result — `Some` exactly once, on the pass after the task publishes it, + /// Drain the startup result — `Some` exactly once, on the pass after the task publishes it, /// then `None`. The ride loop maps any absent sensor to a warning flag (issue #504). pub fn take_presence(&self) -> Option { self.0.presence.try_take() From 870cf8499e4c84ba264cfb87a547a5fc7f0c0e0b Mon Sep 17 00:00:00 2001 From: timohueser Date: Sat, 12 Sep 2026 13:12:38 +0200 Subject: [PATCH 2/3] fix(gps): use controlled GNSS stop and start over Qwiic --- firmware/obc-fw-nrf54l/src/ride.rs | 2 +- firmware/obc-fw-nrf54l/src/sensors.rs | 90 +++++++++++-------------- firmware/obc-platform/src/sensor_hub.rs | 9 ++- firmware/obc-sensors/src/ubx.rs | 54 +++++---------- 4 files changed, 62 insertions(+), 93 deletions(-) diff --git a/firmware/obc-fw-nrf54l/src/ride.rs b/firmware/obc-fw-nrf54l/src/ride.rs index 5500ce419..004203cad 100644 --- a/firmware/obc-fw-nrf54l/src/ride.rs +++ b/firmware/obc-fw-nrf54l/src/ride.rs @@ -733,7 +733,7 @@ impl RideExec { } } -/// The GPS power state the ride wants: deep-sleep when not tracking, full-power fixes while riding, or +/// The GPS power state the ride wants: stopped GNSS when not tracking, full-power fixes while riding, or /// the M10's low-power tracking when the `power_saver` toggle is on. Recomputed each frame in /// [`run_app`] and pushed to the sensor task (via [`SensorControl::set_power`]) only on a change. /// Real-sensor build only — the `synth` / `debug-uart` feeds have no power-managed receiver. diff --git a/firmware/obc-fw-nrf54l/src/sensors.rs b/firmware/obc-fw-nrf54l/src/sensors.rs index 35299fb0c..c44dc40ac 100644 --- a/firmware/obc-fw-nrf54l/src/sensors.rs +++ b/firmware/obc-fw-nrf54l/src/sensors.rs @@ -24,7 +24,7 @@ //! heading is **never stored** — it only orients a heading-up *map while the rider is stopped*. So it //! runs on its **own cadence, decoupled from the GPS fix**: ~5 Hz while stationary (lively as you //! rotate the device by hand, independent of a slow / power-saving fix rate), and silent while moving -//! (the GPS course is the heading then) or idle (the receiver is asleep). See [`sensor_task`]. +//! (the GPS course is the heading then) or idle (GNSS processing is stopped). See [`sensor_task`]. //! //! ## Event-driven, with a robust fallback (the "no fix" story) //! The task waits on the **TX-Ready edge** so it does **zero** bus work between fixes. But TX-Ready @@ -36,12 +36,12 @@ //! simply pauses. Every stage logs over RTT (defmt) so acquisition is watchable live. //! //! ## Power management -//! Continuous tracking is ~20 mA — left on while idle it would flatten the pack in days. So after one -//! **boot fix** (which sets the clock + warms the ephemeris), the task follows the app's -//! [`GpsPower`] request: **deep-sleep** (`RXM-PMREQ` backup, ~µA, zero bus traffic) whenever a ride -//! isn't running, waking on a DDC poke for a fast *warm* fix when one starts; full-power fixes while -//! riding, or the M10's on-chip **low-power** tracking when the `power_saver` toggle is on. The -//! `RXM-PMREQ` / `CFG-PM` encodings live host-tested in [`obc_sensors::ubx`]. +//! After boot acquisition, the task follows the app's [`GpsPower`] request. When idle, it sends +//! `CFG-RST` controlled GNSS stop and parks without DDC polling. Tracking resumes with controlled +//! GNSS start, which works over I²C and retains receiver configuration and navigation data. This +//! is not backup sleep; idle current has not been measured. While riding, the task requests full +//! power or the M10's on-chip low-power tracking when `power_saver` is on. The command encodings +//! live host-tested in [`obc_sensors::ubx`]. use defmt::{debug, error, info, warn}; use embassy_futures::select::{select, select4, Either, Either4}; @@ -80,7 +80,7 @@ const ACC_CAP: usize = 300; /// Bound on the boot-fix acquisition: the task holds awake at most this long for the first fix — /// which sets the clock + warms the ephemeris — before dropping into the power-managed steady state, -/// so a boot under cover (no sky) still eventually deep-sleeps when idle. +/// so a boot under cover (no sky) still eventually stops GNSS processing when idle. const BOOT_ACQUIRE_TIMEOUT_S: u64 = 150; /// Per-board **hard-iron offset** (µT) subtracted from each magnetometer axis before the heading. @@ -140,9 +140,9 @@ struct FixState { /// /// 1. **Boot acquisition** — hold awake until the first valid fix (which sets the clock + warms the /// ephemeris) or [`BOOT_ACQUIRE_TIMEOUT_S`], **ignoring** the app's power request so an idle -/// boot still gets one fix before it can deep-sleep. -/// 2. **Steady state** — honour the app's [`GpsPower`] request: deep-sleep (`RXM-PMREQ` backup, zero -/// bus traffic) when idle; full- (or `power_saver` low-) power fixes while riding. Each waking +/// boot gets an acquisition window before GNSS can stop. +/// 2. **Steady state** — honour the app's [`GpsPower`] request: stop GNSS processing with no DDC +/// polling when idle; full- (or `power_saver` low-) power fixes while riding. Each waking /// cycle waits for a TX-Ready edge / poll timeout / rate change / power change, then drains + /// publishes through [`drain_and_publish`]. While **riding and stationary** it *also* ticks the /// compass on its own [`COMPASS_INTERVAL_MS`] cadence (the heading isn't logged, so it's decoupled @@ -167,6 +167,7 @@ pub async fn sensor_task(mut twim: Twim<'static>, mut txready: Input<'static>, l configure_icm20948(&mut twim, addr).await; } if gps_ok { + set_gnss_running(&mut twim, true).await; configure_m10(&mut twim, DEFAULT_INTERVAL_S).await; } else { warn!("sensors: GPS not answering — retrying during boot acquisition"); @@ -190,7 +191,7 @@ pub async fn sensor_task(mut twim: Twim<'static>, mut txready: Input<'static>, l // --- Phase 1: boot acquisition. Hold awake until the first valid fix or a bounded timeout, // ignoring the app's power request — so the clock gets set and the ephemeris warms even on an idle - // boot, before the steady state below is allowed to deep-sleep. --- + // boot, before the steady state below can stop GNSS processing. --- info!("sensors: boot acquisition — holding awake for the first fix (≤ {=u64}s)", BOOT_ACQUIRE_TIMEOUT_S); let boot_deadline = Instant::now() + Duration::from_secs(BOOT_ACQUIRE_TIMEOUT_S); loop { @@ -205,6 +206,7 @@ pub async fn sensor_task(mut twim: Twim<'static>, mut txready: Input<'static>, l } gps_ok = probe_m10(&mut twim).await; if gps_ok { + set_gnss_running(&mut twim, true).await; configure_m10(&mut twim, interval_s).await; presence.gps = true; link.dispatch_presence(presence); @@ -226,39 +228,38 @@ pub async fn sensor_task(mut twim: Twim<'static>, mut txready: Input<'static>, l link.dispatch_presence(presence); } - // --- Phase 2: power-managed steady state. Honour the app's requested GpsPower — deep-sleep when + // --- Phase 2: power-managed steady state. Honour the app's requested GpsPower — stop GNSS when // idle, full / low-power fixes while riding — and keep streaming fixes. --- let mut power = GpsPower::Active; - let mut asleep = false; // so backup is commanded once on entry, not re-sent each parked iteration - // Absolute deadline for the next DDC poll fallback. Absolute (not a fresh `Timer::after` each - // iteration) so the stationary compass ticks below don't keep restarting it — which would starve - // a TX-Ready-less receiver's stationary fixes. Reset only after an actual fix cycle / rate change. + // Send STOP once per idle entry, even if its write fails. + let mut parked = false; + // Absolute deadline: compass ticks must not restart it and starve DDC fallback polling. let mut next_poll = Instant::now() + poll_deadline(interval_s); loop { if power == GpsPower::Sleep { - if !asleep { - enter_backup(&mut twim).await; - asleep = true; + if !parked { + set_gnss_running(&mut twim, false).await; + parked = true; } - // Asleep: zero DDC traffic. Wait only for a power change (or a rate change to apply on - // the next wake — `CFG-RATE` can't take effect while the receiver is in backup). + // No DDC polling while idle. Apply rate changes when tracking resumes. match select(link.wait_power(), link.wait_rate()).await { Either::First(p) => power = p, Either::Second(s) => { interval_s = s.max(1); - continue; // still asleep — re-park; the new rate applies on the next wake + continue; // still idle — re-park; apply the new rate on resume } } if power == GpsPower::Sleep { continue; // a redundant Sleep request — stay parked } - // Woken → poke the receiver out of backup and re-assert config at the current rate/mode. - asleep = false; - wake_receiver(&mut twim).await; + parked = false; + set_gnss_running(&mut twim, true).await; + // Configuration reads use a separate buffer; discard any partial pre-stop frame. + acc_len = 0; configure_m10(&mut twim, interval_s).await; set_power_mode(&mut twim, power).await; - st.had_fix = false; // re-acquiring from a warm start - st.stationary = false; // motion state unknown until the first warm fix → compass off + st.had_fix = false; // acquiring again after GNSS start + st.stationary = false; // motion state unknown until the first new fix → compass off next_poll = Instant::now() + poll_deadline(interval_s); continue; } @@ -294,7 +295,7 @@ pub async fn sensor_task(mut twim: Twim<'static>, mut txready: Input<'static>, l if p != power { power = p; if power == GpsPower::Sleep { - info!("sensors: tracking stopped → GPS will deep-sleep"); + info!("sensors: tracking stopped → requesting GNSS stop"); } else { info!("sensors: GPS power → {=str}", power_name(power)); set_power_mode(&mut twim, power).await; @@ -382,8 +383,8 @@ async fn drain_and_publish( // The key acquisition line — watch fixType climb 0→3 and hAcc fall as the receiver locks. debug!( - "NAV-PVT fix={=u8} sats={=u8} hAcc={=u32}mm pDOP={=u16} lat={=i32} lon={=i32}", - pvt.fix_type, pvt.num_sv, pvt.hacc_mm, pvt.pdop, pvt.lat, pvt.lon + "NAV-PVT iTOW={=u32} fix={=u8} sats={=u8} hAcc={=u32}mm pDOP={=u16} lat={=i32} lon={=i32}", + pvt.itow, pvt.fix_type, pvt.num_sv, pvt.hacc_mm, pvt.pdop, pvt.lat, pvt.lon ); // Publish the receiver's UTC time the moment it's valid + fully resolved — **before** @@ -440,30 +441,19 @@ fn power_name(p: GpsPower) -> &'static str { } } -/// Put the M10 into **backup** deep sleep — `RXM-PMREQ`, infinite duration. The -/// receiver keeps its RTC + ephemeris on ~µA and wakes on the next DDC activity, so the restart is a -/// fast *warm* fix. Best-effort: a failed write is logged, not fatal. -async fn enter_backup(twim: &mut Twim<'static>) { - let mut frame = [0u8; 24]; - let Some(n) = ubx::pmreq_backup(&mut frame) else { return }; +/// Send a controlled GNSS start/stop without clearing receiver configuration or navigation data. +/// CFG-RST has no ACK. A successful write confirms only that the command was sent. +async fn set_gnss_running(twim: &mut Twim<'static>, running: bool) { + let mut frame = [0u8; 12]; + let Some(n) = ubx::cfg_gnss_running(&mut frame, running) else { return }; + let action = if running { "start" } else { "stop" }; if twim.write(M10_ADDR, &frame[..n]).await.is_err() { - warn!("sensors: RXM-PMREQ (sleep) write failed — GPS may keep tracking"); + warn!("sensors: CFG-RST GNSS {=str} write failed", action); } else { - info!("sensors: GPS → deep sleep (RXM-PMREQ backup); zero bus traffic until tracking resumes"); + info!("sensors: CFG-RST GNSS {=str} sent", action); } } -/// Wake the M10 from backup: any DDC activity wakes it, but the first transaction can be -/// lost while it powers up, so poke the byte-count register a few times with a short settle. -async fn wake_receiver(twim: &mut Twim<'static>) { - for _ in 0..3 { - let mut cnt = [0u8; 2]; - let _ = twim.write_read(M10_ADDR, &[DDC_COUNT_REG], &mut cnt).await; - Timer::after_millis(20).await; - } - info!("sensors: GPS woken from backup"); -} - /// Set the M10's tracking power mode: full power, or the on-chip low-power tracking when /// `power_saver` is on. Best-effort VALSET, ACK-logged like the other config keys — **verify the /// `CFG-PM-OPERATEMODE` key + value semantics on first bring-up** (see [`ubx::KEY_PM_OPERATEMODE`]). diff --git a/firmware/obc-platform/src/sensor_hub.rs b/firmware/obc-platform/src/sensor_hub.rs index 98ac52a2b..fef847f9d 100644 --- a/firmware/obc-platform/src/sensor_hub.rs +++ b/firmware/obc-platform/src/sensor_hub.rs @@ -70,9 +70,8 @@ pub struct SensorPresence { } /// The GPS receiver's requested power state. The ride loop derives one from whether a ride is active -/// and the `power_saver` toggle, and the sensor task drives the M10 to match: deep sleep when idle -/// (~µA vs. the ~20 mA of continuous tracking), full-power fixes while riding, or the M10's on-chip -/// low-power tracking when `power_saver` is on. +/// and the `power_saver` toggle. The sensor task requests stopped GNSS processing when idle, +/// full-power fixes while riding, or the M10's on-chip low-power tracking when `power_saver` is on. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum GpsPower { /// Riding, full-power continuous fixes at the configured rate. @@ -80,8 +79,8 @@ pub enum GpsPower { /// Riding with `power_saver` on — the M10's low-power tracking mode (lower power, same rate, at /// the cost of some fix latency). LowPower, - /// Not tracking — deep sleep (`RXM-PMREQ` backup); woken on the next [`Active`](GpsPower::Active) - /// / [`LowPower`](GpsPower::LowPower) request for a fast warm fix. + /// Not tracking — stop GNSS processing and park host polling. Resume on the next + /// [`Active`](GpsPower::Active) / [`LowPower`](GpsPower::LowPower) request. Sleep, } diff --git a/firmware/obc-sensors/src/ubx.rs b/firmware/obc-sensors/src/ubx.rs index 7b403eb6a..e1b79fd21 100644 --- a/firmware/obc-sensors/src/ubx.rs +++ b/firmware/obc-sensors/src/ubx.rs @@ -43,10 +43,8 @@ pub const ID_ACK_NAK: u8 = 0x00; pub const CLASS_CFG: u8 = 0x06; pub const ID_CFG_VALSET: u8 = 0x8A; -/// `UBX-RXM` class + the `PMREQ` (power-management request) message id — the deep-sleep command the -/// driver issues when tracking stops. See [`pmreq_backup`]. -pub const CLASS_RXM: u8 = 0x02; -pub const ID_RXM_PMREQ: u8 = 0x41; +/// Controlled GNSS start/stop command. See [`cfg_gnss_running`]. +pub const ID_CFG_RST: u8 = 0x04; // Little-endian field readers — UBX is little-endian throughout. Each returns 0 if the slice is // too short (callers gate on length first). @@ -404,24 +402,12 @@ pub fn valset_u16(out: &mut [u8], key: u32, val: u16) -> Option { frame(out, CLASS_CFG, ID_CFG_VALSET, &payload) } -/// `RXM-PMREQ` `flags`: request **backup** mode (deep sleep, RTC + RAM retained) and **force** it -/// even with active comms. Woken by activity on the comms port (the driver pokes the DDC). -const PMREQ_FLAG_BACKUP: u32 = 0x02; -const PMREQ_FLAG_FORCE: u32 = 0x04; - -/// Build a `UBX-RXM-PMREQ` frame requesting **backup** (deep sleep) for an infinite duration — -/// the M10 retains its RTC + ephemeris on ~microamps and wakes on the next DDC activity (a fast -/// *warm* fix). 16-byte v0 payload: `version(1) | reserved(3) | duration(4 LE, 0 = until woken) | -/// flags(4 LE, backup|force) | wakeupSources(4, 0 = comms activity)`. Returns the frame length -/// written to `out` (24 B), or `None` if `out` is too small. -pub fn pmreq_backup(out: &mut [u8]) -> Option { - let mut payload = [0u8; 16]; - payload[0] = 0x00; // version 0 - // payload[1..4] reserved, payload[4..8] duration = 0 (infinite, until woken) - let flags = PMREQ_FLAG_BACKUP | PMREQ_FLAG_FORCE; - payload[8..12].copy_from_slice(&flags.to_le_bytes()); - // payload[12..16] wakeupSources = 0 — any traffic on the (I²C) comms port wakes it. - frame(out, CLASS_RXM, ID_RXM_PMREQ, &payload) +/// Start or stop GNSS tasks without clearing navigation data or receiver configuration. +/// `CFG-RST` payload: zero `navBbrMask`, controlled start/stop mode, reserved zero. +/// The receiver does not acknowledge this command. Returns 12 bytes, or `None` if too small. +pub fn cfg_gnss_running(out: &mut [u8], running: bool) -> Option { + let mode = if running { 0x09 } else { 0x08 }; + frame(out, CLASS_CFG, ID_CFG_RST, &[0, 0, mode, 0]) } /// Common VALSET payload prefix (first 8 bytes): `version=0 | layers=RAM | reserved(2) | key(4 LE)`. @@ -612,21 +598,15 @@ mod tests { } #[test] - fn pmreq_backup_frames_an_infinite_backup_request() { - let mut out = [0u8; 24]; - let n = pmreq_backup(&mut out).unwrap(); - match scan_ubx(&out[..n]) { - Scan::Frame { frame, consumed } => { - assert_eq!((frame.class, frame.id), (CLASS_RXM, ID_RXM_PMREQ)); - assert_eq!(consumed, n); - assert_eq!(frame.payload.len(), 16, "v0 PMREQ payload"); - assert_eq!(frame.payload[0], 0, "version 0"); - assert_eq!(&frame.payload[4..8], &[0, 0, 0, 0], "duration 0 = until woken"); - let flags = - u32::from_le_bytes([frame.payload[8], frame.payload[9], frame.payload[10], frame.payload[11]]); - assert_eq!(flags, 0x06, "backup | force"); - } - other => panic!("expected a frame, got {other:?}"), + fn gnss_control_preserves_navigation_data_and_uses_controlled_modes() { + for (running, expected) in [ + (false, [0xb5, 0x62, 0x06, 0x04, 0x04, 0, 0, 0, 0x08, 0, 0x16, 0x74]), + (true, [0xb5, 0x62, 0x06, 0x04, 0x04, 0, 0, 0, 0x09, 0, 0x17, 0x76]), + ] { + let mut out = [0u8; 12]; + assert_eq!(cfg_gnss_running(&mut out, running), Some(expected.len())); + assert_eq!(out, expected); + assert_eq!(cfg_gnss_running(&mut out[..11], running), None); } } From 8796e8f0e67898a67843059ae7b3999906acd4f4 Mon Sep 17 00:00:00 2001 From: timohueser Date: Sat, 12 Sep 2026 13:12:38 +0200 Subject: [PATCH 3/3] docs: describe Qwiic GPS idle and reset behavior --- docs/content/software/architecture.md | 2 +- firmware/obc-fw-nrf54l/README.md | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/content/software/architecture.md b/docs/content/software/architecture.md index 5f91ff2d6..dfba6f6f0 100644 --- a/docs/content/software/architecture.md +++ b/docs/content/software/architecture.md @@ -291,7 +291,7 @@ The device also wakes for input, sensor data, and the watchdog guard. arm the next wake, sleep again - idle (nothing animating · GPS asleep): just the ~10 s watchdog-feed guard tick + idle (no animation · GNSS stopped): ~10 s watchdog-feed guard
The device sleeps between events. A hardware timer generates the display COM signal without CPU work.
diff --git a/firmware/obc-fw-nrf54l/README.md b/firmware/obc-fw-nrf54l/README.md index 9374e402d..85c1243e2 100644 --- a/firmware/obc-fw-nrf54l/README.md +++ b/firmware/obc-fw-nrf54l/README.md @@ -188,7 +188,19 @@ the GPS configuration before it reads fixes. The startup sensor-warning bundle i or at the deadline if GPS remains absent. This can delay the altimeter and compass warnings too; their original probe results are retained. A successful first GPS probe publishes immediately. This is a one-time startup result, not a report of live sensor availability. Intentional idle -backup sleep still produces no DDC polling. +GNSS stop still produces no DDC polling. + +The Qwiic prototype has no GPS wake or reset wire. Idle sends `UBX-CFG-RST` controlled GNSS stop; +tracking resumes with controlled GNSS start. Startup also sends START after the receiver answers, +so an MCU reset can recover a receiver left stopped by the previous session. The driver sends +configuration after START. These operations retain receiver configuration and navigation data. +They do not enter backup sleep, and their idle current has not been measured. The receiver does +not acknowledge CFG-RST; logs report a command write, while new NAV-PVT epochs confirm acquisition. +The existing `power_saver` tracking mode is unchanged. + +An older image can leave the receiver in indefinite software standby. I²C traffic is not a +supported wake source for that mode. If the receiver remains absent, remove its power before +starting this image. See the [SAM-M10Q integration manual, sections 3.3 and 3.5.3.3](https://content.u-blox.com/sites/default/files/documents/SAM-M10Q_IntegrationManual_UBX-22020019.pdf). ## Build & flash