Files
ZephCore/zephcore/adapters/gps/gps_power.cpp
T
2026-09-27 16:33:58 +02:00

599 lines
24 KiB
C++

/*
* SPDX-License-Identifier: MIT
* GPS manager: module power (see gps_internal.h).
*/
#include "gps_internal.h"
#include "ZephyrGPSManager.h"
#include <zephyr/logging/log.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/regulator.h>
#include <zephyr/drivers/i2c.h>
#include <zephyr/pm/device.h>
LOG_MODULE_DECLARE(zephcore_gps, CONFIG_ZEPHCORE_GPS_LOG_LEVEL);
/* ========== GPS Power Strategy ==========
* Module power is GPIO/regulator controlled — GNSS driver PM is not used
* for the module itself:
* - Wio Tracker L1 (L76K): P1.09 is the module's WAKEUP pin, not a supply
* switch. Per the L76K hardware design: WAKEUP is a digital input, active
* low with an internal pull-up, that "enters or exits Standby mode". In
* Standby the RF is powered off but the internal core and I/O power domain
* stay active, so VCC is never removed and ephemeris/almanac/RTC survive —
* every wake is a warm start, not a cold one. (Backup mode, the deeper
* state, requires cutting VCC while V_BCKP holds the RTC domain; this
* board has no VCC switch, so Standby is the floor available to us.)
* - T1000-E (AG3335): GPS_EN de-asserted + VRTC asserted = warm standby (ephemeris
* preserved via backup RAM, ~1-2µA VRTC current)
* - All boards: gps-enable alias → GPIO power control
*
* CONFIG_PM_DEVICE is on globally, but nothing suspends automatically —
* system-managed suspend is compiled only under CONFIG_PM (off everywhere).
* This manager makes exactly two kinds of PM calls, both main-thread only:
* - a one-time RESUME of the GNSS device at boot (gnss-nmea-generic inits
* suspended under CONFIG_PM_DEVICE and never opens its pipe otherwise);
* - suspend/resume of the GNSS UARTE around standby/off (an armed UARTE RX
* holds HFCLK ≈0.5-1 mA on nRF52840 even with the module powered off).
* The old "PM broke GPS" deadlock was modem_chat_run_script() being reached
* from the system workqueue via driver PM hooks — the air530z driver is
* PM-less now and every PM call here stays on the main thread. */
/* ========== GPS Power GPIO Control ==========
* These are unconditional (not gated by HAS_GNSS) because
* gps_power_off_for_shutdown() must be available for System OFF
* even on boards without a GNSS driver.
*
* IMPORTANT: Do NOT touch GPIO during init! The GNSS driver needs the GPS
* to be powered and outputting NMEA for the modem pipe to work.
* We only configure GPIO lazily on first power-off request.
*
* Board-specific pins (defined in board overlays as gps-enable alias):
* - T1000-E: P1.11 (GPS_EN), P0.8 (GPS_VRTC_EN), P1.15 (GPS_RESET), P1.12 (GPS_SLEEP_INT)
* - Wio Tracker L1: P1.09 (GPS power, shared with luatos,air530z on-off-gpios)
*/
#if HAS_GPS_POWER_CONTROL
static const struct gpio_dt_spec gps_enable_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(gps_enable), gpios);
#endif
/* GPS powered by a PMU regulator rail instead of a discrete enable GPIO (e.g.
* LilyGo T-Beam: GPS is on the AXP2101 ALDO3 rail). Selected via the chosen
* `zephcore,gps-power` node pointing at the regulator. It is the power switch
* on every path, duty-cycle standby included; the AXP2101 VBACKUP charger
* (below) keeps the receiver's backup domain alive across each cut, so a wake
* is a warm start. */
#if HAS_GPS_POWER_REGULATOR
static const struct device *const gps_power_reg =
DEVICE_DT_GET(DT_CHOSEN(zephcore_gps_power));
/* Tracks our intended rail state so enable/disable stay balanced (idempotent).
* Starts true: the rail is `regulator-boot-on`, so it is already up at boot. */
static bool gps_reg_enabled = true;
#endif
/* AXP2101 backup (button-battery) charger — feeds the GPS receiver's V_BCKP
* domain so ephemeris/RTC survive main-rail (ALDO3) power cuts, giving a
* warm/hot re-fix instead of a cold start each duty cycle. The Zephyr regulator
* driver doesn't expose VBACKUP, so enable it with raw I2C at boot (mirrors
* Arduino enablePowerOutput(XPOWERS_VBACKUP) + setPowerChannelVoltage 3.3V).
* Selected via chosen `zephcore,gps-backup-pmu` pointing at the AXP2101 node. */
#if DT_NODE_EXISTS(DT_CHOSEN(zephcore_gps_backup_pmu))
#define AXP2101_REG_CHG_GAUGE_WDT_CTRL 0x18U /* bit 2 = button-battery charge enable */
#define AXP2101_BTN_CHARGE_ENABLE BIT(2)
#define AXP2101_REG_BTN_BAT_CHG_VOL_SET 0x6AU /* low 3 bits: (mV - 2600) / 100 */
#define AXP2101_BTN_VOL_3V3 0x07U /* (3300 - 2600) / 100 */
static int gps_backup_charger_init(void)
{
static const struct i2c_dt_spec axp = I2C_DT_SPEC_GET(DT_CHOSEN(zephcore_gps_backup_pmu));
if (!device_is_ready(axp.bus)) {
LOG_WRN("GPS backup: AXP2101 I2C bus not ready");
return 0;
}
/* Set the backup-charge target to 3.3V (low 3 bits), then enable the
* charger. Read-modify-write so the fuel-gauge enable (bit 3 of 0x18) and
* the other 0x6A bits are preserved. */
i2c_reg_update_byte_dt(&axp, AXP2101_REG_BTN_BAT_CHG_VOL_SET, 0x07U, AXP2101_BTN_VOL_3V3);
i2c_reg_update_byte_dt(&axp, AXP2101_REG_CHG_GAUGE_WDT_CTRL,
AXP2101_BTN_CHARGE_ENABLE, AXP2101_BTN_CHARGE_ENABLE);
LOG_INF("GPS backup: AXP2101 VBACKUP charger enabled (3.3V)");
return 0;
}
/* After the MFD/I2C is up (POST_KERNEL ~86); APPLICATION is safely later. */
SYS_INIT(gps_backup_charger_init, APPLICATION, 50);
#endif
/* T1000-E specific GPS control pins */
#if DT_NODE_EXISTS(DT_ALIAS(gps_vrtc_enable))
static const struct gpio_dt_spec gps_vrtc_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(gps_vrtc_enable), gpios);
#define HAS_GPS_VRTC 1
#else
#define HAS_GPS_VRTC 0
#endif
#if DT_NODE_EXISTS(DT_ALIAS(gps_reset))
static const struct gpio_dt_spec gps_reset_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(gps_reset), gpios);
#define HAS_GPS_RESET 1
#else
#define HAS_GPS_RESET 0
#endif
#if DT_NODE_EXISTS(DT_ALIAS(gps_sleep_int))
static const struct gpio_dt_spec gps_sleep_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(gps_sleep_int), gpios);
#define HAS_GPS_SLEEP 1
#else
#define HAS_GPS_SLEEP 0
#endif
/* GPS RTC interrupt pin — held de-asserted during normal operation */
#if DT_NODE_EXISTS(DT_ALIAS(gps_rtc_int))
static const struct gpio_dt_spec gps_rtcint_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(gps_rtc_int), gpios);
#define HAS_GPS_RTCINT 1
#else
#define HAS_GPS_RTCINT 0
#endif
/* GPS RESETB (active-LOW reset, declared GPIO_ACTIVE_LOW) — must be
* INPUT_PULLUP for normal operation; the pull-up is a physical setting.
* Without the pull-up, this pin floats LOW and holds the AG3335 in permanent
* reset, preventing any UART output. */
#if DT_NODE_EXISTS(DT_ALIAS(gps_resetb))
static const struct gpio_dt_spec gps_resetb_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(gps_resetb), gpios);
#define HAS_GPS_RESETB 1
#else
#define HAS_GPS_RESETB 0
#endif
/* T1000-E has extra GPS control pins that require a specific init sequence */
#define HAS_T1000_GPS_CONTROL (HAS_GPS_VRTC || HAS_GPS_RESET || HAS_GPS_SLEEP)
#if HAS_GPS_POWER_CONTROL
static bool gps_gpio_configured = false;
#endif
/* The off levels, shared by gps_power_control(false) and System OFF: reset
* and resetb asserted (the module held in reset while its supply is gated,
* so it starts cleanly on the next power-on), the enable and rtcint
* de-asserted, VRTC de-asserted unless keep_vrtc (warm standby).
*
* The sleep line is the one difference: runtime standby/off leaves it
* asserted, System OFF drops it -- as upstream, whose T1000-E sleep_gps() and
* stop_gps() drive GPS_SLEEP_INT HIGH and only powerOff() drives it LOW.
*
* gpio_pin_configure_dt() on every line: a boot with the GPS off never ran
* the power-on path, so the pins may not be outputs yet. */
static void gps_drive_off_levels(bool keep_vrtc, bool system_off)
{
#if HAS_GPS_RESET
if (gpio_is_ready_dt(&gps_reset_gpio)) {
gpio_pin_configure_dt(&gps_reset_gpio, GPIO_OUTPUT_ACTIVE);
}
#endif
#if HAS_GPS_VRTC
if (!keep_vrtc && gpio_is_ready_dt(&gps_vrtc_gpio)) {
gpio_pin_configure_dt(&gps_vrtc_gpio, GPIO_OUTPUT_INACTIVE);
}
#else
ARG_UNUSED(keep_vrtc);
#endif
#if HAS_GPS_POWER_CONTROL
/* INACTIVE, not LOW: physical LOW leaves an active-low enable asserted. */
if (gpio_is_ready_dt(&gps_enable_gpio)) {
gpio_pin_configure_dt(&gps_enable_gpio, GPIO_OUTPUT_INACTIVE);
gps_gpio_configured = true;
}
#endif
#if HAS_GPS_SLEEP
if (system_off && gpio_is_ready_dt(&gps_sleep_gpio)) {
gpio_pin_configure_dt(&gps_sleep_gpio, GPIO_OUTPUT_INACTIVE);
}
#else
ARG_UNUSED(system_off);
#endif
#if HAS_GPS_RTCINT
if (gpio_is_ready_dt(&gps_rtcint_gpio)) {
gpio_pin_configure_dt(&gps_rtcint_gpio, GPIO_OUTPUT_INACTIVE);
}
#endif
#if HAS_GPS_RESETB
/* Declared GPIO_ACTIVE_LOW, so ACTIVE drives it LOW. */
if (gpio_is_ready_dt(&gps_resetb_gpio)) {
gpio_pin_configure_dt(&gps_resetb_gpio, GPIO_OUTPUT_ACTIVE);
}
#endif
}
/* GPS power control with warm standby support.
* @param on true = power on, false = power off
* @param keep_vrtc When powering off: true = keep VRTC alive (warm standby,
* preserves ephemeris/almanac/RTC for fast re-acquisition),
* false = full power-off (cold start on next wake).
* Only relevant on T1000-E (HAS_GPS_VRTC); ignored on other boards. */
void gps_power_control(bool on, bool keep_vrtc)
{
#if HAS_GPS_POWER_REGULATOR
/* Master power rail (PMU regulator). Idempotent enable/disable so the
* refcount stays balanced regardless of how often this is called. */
if (on != gps_reg_enabled && device_is_ready(gps_power_reg)) {
int ret = on ? regulator_enable(gps_power_reg)
: regulator_disable(gps_power_reg);
if (ret == 0) {
gps_reg_enabled = on;
LOG_INF("GPS power %s (regulator)", on ? "ON" : "OFF");
} else {
LOG_WRN("GPS regulator %s failed: %d", on ? "enable" : "disable", ret);
}
}
#endif
#if HAS_GPS_POWER_CONTROL
/* Direct GPIO power control — works on all boards.
* We toggle the GPS power pin ourselves rather than using driver PM
* (driver PM can hang on modem_pipe_close / modem_chat_run_script).
* The GNSS driver's modem pipe stays open.
*
* T1000-E (HAS_GPS_VRTC): Use warm standby (keep VRTC) for app toggle
* so UART/chip state is preserved. Matches Arduino sleep_gps().
* Simple boards (Wio etc.): Full power off/on via GPS_EN. */
if (on) {
#if HAS_T1000_GPS_CONTROL
/* T1000-E power-on sequence (from Arduino target.cpp start_gps())
* Must follow this exact order with delays. Levels are ASSERTED /
* DE-ASSERTED, not physical: Arduino states them as HIGH/LOW because
* its pins are all active-high, while this block is also reached by
* boards whose lines are active-low (see the gate below).
* 1. GPS_EN asserted, delay 10ms
* 2. GPS_VRTC_EN asserted, delay 10ms (critical - RTC power)
* 3. GPS_RESET asserted, delay 10ms, then released
* 4. GPS_SLEEP_INT asserted
*
* Despite the name this is not a T1000-E-only path:
* HAS_T1000_GPS_CONTROL is (HAS_GPS_VRTC || HAS_GPS_RESET ||
* HAS_GPS_SLEEP), so a bare gps-reset alias is enough to route a
* board here. heltec_wifi_lora32_v4, _v43 and thinknode_m9 all arrive
* this way, and all three declare gps-enable active-low.
*/
if (gpio_is_ready_dt(&gps_enable_gpio)) {
gpio_pin_configure_dt(&gps_enable_gpio, GPIO_OUTPUT_ACTIVE);
}
k_msleep(10);
#if HAS_GPS_VRTC
if (gpio_is_ready_dt(&gps_vrtc_gpio)) {
gpio_pin_configure_dt(&gps_vrtc_gpio, GPIO_OUTPUT_ACTIVE);
}
k_msleep(10);
#endif
#if HAS_GPS_RESET
if (gpio_is_ready_dt(&gps_reset_gpio)) {
gpio_pin_configure_dt(&gps_reset_gpio, GPIO_OUTPUT_ACTIVE);
k_msleep(10);
gpio_pin_set_dt(&gps_reset_gpio, 0); /* Release reset (logical) */
}
#endif
#if HAS_GPS_SLEEP
if (gpio_is_ready_dt(&gps_sleep_gpio)) {
gpio_pin_configure_dt(&gps_sleep_gpio, GPIO_OUTPUT_ACTIVE);
}
#endif
#if HAS_GPS_RTCINT
/* GPS_RTC_INT (P0.15) — held de-asserted during normal operation */
if (gpio_is_ready_dt(&gps_rtcint_gpio)) {
gpio_pin_configure_dt(&gps_rtcint_gpio, GPIO_OUTPUT_INACTIVE);
}
#endif
#if HAS_GPS_RESETB
/* GPS_RESETB (P1.14) — active-LOW reset, must be pulled HIGH.
* INPUT_PULLUP de-asserts reset so the AG3335 can boot.
* Without this the pin floats LOW → chip stuck in reset → no UART. */
if (gpio_is_ready_dt(&gps_resetb_gpio)) {
gpio_pin_configure_dt(&gps_resetb_gpio, GPIO_INPUT | GPIO_PULL_UP);
}
#endif
gps_gpio_configured = true;
LOG_INF("GPS power ON (T1000-E sequence)");
#else
/* Simple boards - just GPS_EN */
if (!gps_gpio_configured) {
if (gpio_is_ready_dt(&gps_enable_gpio)) {
/* ACTIVE, not HIGH: gpio_pin_set_dt() below is
* logical, so a physical init flag here would assert
* the opposite level on an active-low gps-enable. */
gpio_pin_configure_dt(&gps_enable_gpio, GPIO_OUTPUT_ACTIVE);
gps_gpio_configured = true;
LOG_INF("GPS power GPIO configured, set ACTIVE");
} else {
LOG_WRN("GPS power GPIO not ready");
return;
}
} else {
gpio_pin_set_dt(&gps_enable_gpio, 1);
LOG_INF("GPS power ON");
}
#endif
} else {
gps_drive_off_levels(keep_vrtc, false);
#if HAS_GPS_VRTC
LOG_INF("GPS power OFF (%s)", keep_vrtc ?
"standby — VRTC retained" : "full");
#else
LOG_INF("GPS power OFF");
#endif
}
#else
ARG_UNUSED(keep_vrtc);
#endif
}
/* Put every GPS control line this board declares into its System OFF state
* (see gps_drive_off_levels()), the regulator rail off and VRTC off too.
* Configures the pins, so it works even if gps_power_control() never ran. */
void gps_power_off_for_shutdown(void)
{
#if HAS_GPS_POWER_REGULATOR
if (gps_reg_enabled && device_is_ready(gps_power_reg)) {
regulator_disable(gps_power_reg);
gps_reg_enabled = false;
}
#endif
gps_drive_off_levels(false, true);
}
#if HAS_GNSS
/* ========== Software Sleep/Wake (no GPIO required) ==========
*
* On boards without dedicated GPS power control (e.g. RAK3401 where the
* 3V3_S rail is shared with the LoRa FEM), we send vendor-specific UART
* commands to put the GPS module into low-power mode.
*
* Strategy: send BOTH MediaTek and u-blox sleep commands — the module that
* isn't present simply ignores the bytes it doesn't understand.
*
* - MediaTek (e.g. L76B): $PMTK161,0*28\r\n → standby (~1mA), wake on UART
* - u-blox ZOE-M8Q (RAK12500): UBX-RXM-PMREQ → backup (~7µA), wake on UART
*
* Neither reaches a CASIC part (L76K/L76KB/Air530Z): those ignore PMTK, UBX
* and PCAS12 sleep commands alike — verified on hardware, which is why the
* boards carrying them duty-cycle with a power GPIO instead. Note also that
* the RAK1910 is a u-blox MAX-7Q, not an L76K, despite older comments here.
*
* Wake: any byte on UART wakes both modules from their low-power modes.
* After wake, the module resumes outputting NMEA autonomously.
*/
/* HAS_GPS_UART, gps_uart_dev and gps_uart_send are defined near the top of
* this file (see "GPS Feature Detection") — they are needed by the boot-time
* module configuration, which runs long before this section. */
/* ========== GNSS UART Suspend/Resume (device PM) ==========
* nRF UARTE only. An armed UARTE RX holds HFCLK (~0.5-1 mA on nRF52840)
* even when the GPS module is powered off or silent, so standby/off
* suspends the UART device and every wake resumes it first.
*
* Verified symmetric in uart_nrfx_uarte.c under the still-open modem pipe:
* suspend saves the RX-interrupt state, STOPRXes, disables the peripheral
* and applies the sleep pinctrl; resume restores all of it. Other UART
* drivers (legacy nordic,nrf-uart on RAK4631, ESP32) are deliberately not
* gated in — their suspend/resume round-trip is unverified and the HFCLK
* cost is UARTE-specific.
*
* Every GPS UART node must carry a sleep pinctrl state (all boards do):
* without one, suspend fails *after* disabling RX while the PM state stays
* ACTIVE, so the next resume no-ops with -EALREADY — a dead GPS.
*
* Safe on boards WITHOUT GPS power control too, since patch 0010.
* uarte_pm_suspend() used to busy-wait for RXTO with no timeout after
* triggering STOPRX. In interrupt-driven mode RX runs on a 1-byte buffer
* with no ENDRX_STARTRX short, so the receiver stops after every byte until
* the ISR re-arms it — and suspend disables the ENDRX interrupt *before*
* STOPRX, removing the re-arm. Land in that window with bytes still arriving
* and STOPRX hits an already-stopped receiver, no RXTO is generated, and the
* caller spun forever on the main thread (observed: RAK3401 1W repeater on
* 1.16.6, CLI answering only "-> busy"). That is why this gate once required
* power control: boards with it cut the module first, so the line is quiet.
* Boards without it send gps_software_sleep()'s PMTK/UBX commands, which the
* module may ignore (CASIC parts; a MAX-7Q is protocol 14/15, older than the
* UBX-RXM-PMREQ we send) and keep streaming NMEA straight into the suspend.
*
* Patch 0010 bounds that wait (4 ms, then the unconditional
* nrf_uarte_disable() force-stops the receiver) and clears the RX events, so
* the race can no longer hang the caller, and resume re-arms RX from a clean
* state. So every nRF UARTE GPS board releases its UART in standby/off now,
* including an optional GPS port with nothing fitted (xiao_nrf54l15 uart21).
* Bench-tested by the streaming-into-suspend case: devdocs/lld/13. */
#if HAS_GPS_UART && defined(CONFIG_PM_DEVICE) && \
DT_NODE_HAS_COMPAT(DT_BUS(DT_NODELABEL(gnss)), nordic_nrf_uarte)
#define HAS_GPS_UART_PM 1
#else
#define HAS_GPS_UART_PM 0
#endif
/* Suspend/resume the GNSS UART. Main thread only (like all GPS power
* paths — pm_device_action_run() calls the driver synchronously).
* Ordering: resume BEFORE powering the module / sending the wake byte;
* suspend AFTER the module is off / sleep commands were sent. */
#if HAS_GPS_UART_PM
void gps_uart_set_power(bool on)
{
if (!device_is_ready(gps_uart_dev)) {
return;
}
if (!on) {
/* Let the GNSS line go quiet before suspending. Every caller
* cuts module power (GPS_EN low / reset asserted / regulator
* off) immediately before this, but a byte can still be in
* flight. Settle so it finishes and the driver's RX ISR re-arms,
* leaving the receiver armed-and-idle when the suspend's STOPRX
* fires — that state yields RXTO, whereas a just-stopped,
* un-rearmed receiver can produce none and (pre-0010) hung the
* main thread. ~5 ms comfortably covers one character time at
* GNSS baud plus ISR latency; standby happens at most every few
* minutes, so the cost is negligible. Backstop: patch 0010
* bounds the driver's RXTO wait so a missed RXTO can never hang
* us even if a byte still lands in the race window. */
k_msleep(5);
}
int ret = pm_device_action_run(gps_uart_dev,
on ? PM_DEVICE_ACTION_RESUME
: PM_DEVICE_ACTION_SUSPEND);
if (ret == 0) {
LOG_INF("GPS UART %s", on ? "resumed" : "suspended");
} else if (ret != -EALREADY) {
LOG_WRN("GPS UART %s failed: %d", on ? "resume" : "suspend", ret);
}
}
#else
void gps_uart_set_power(bool on) { ARG_UNUSED(on); }
#endif
#if HAS_GPS_UART && !HAS_GPS_POWER_CONTROL && !HAS_GPS_POWER_REGULATOR
/* gps_uart_send() lives near the top of the file — see "GPS Feature
* Detection". Only the sleep/wake commands below are gated on this board
* having no hardware GPS power control. */
/* MediaTek parts: $PMTK161,0*28\r\n → enter standby mode
* Module stops NMEA output and draws ~1mA. Wakes on any UART RX byte.
* Inert on CASIC parts (L76K and relatives) — they have no such command. */
static const uint8_t pmtk_standby[] = "$PMTK161,0*28\r\n";
/* u-blox ZOE-M8Q: UBX-RXM-PMREQ → enter backup mode
* UBX frame: B5 62 | 02 41 | 10 00 | payload(16) | CK_A CK_B
* Payload (protocol 23+, 16 bytes):
* version=0, reserved[3]=0,
* duration=0x00000000 (infinite),
* flags=0x00000006 (backup + force),
* wakeupSources=0x00000028 (uartrx bit3 | extint0 bit5)
*
* THE WAKE SOURCE BIT IS LOAD-BEARING. wakeupSources bit 3 is uartrx; bit 5
* is extint0. This frame previously sent 0x20 — extint0 only — with a comment
* claiming that was "UART RX (bit 5)". It is not. EXTINT is not routed to the
* WisBlock connector on the RAK12500 (RAK's datasheet: only UART/I2C, 1PPS,
* RESET, VDD and GND are connected), so the module was told to sleep forever
* with a wake source that can never be asserted. duration=0 means infinite,
* so it never came back: no NMEA at any baud, and no I2C either, because
* backup mode powers down the DDC interface too. Only a physical power cycle
* recovered it — a reboot does not, since the 3V3_S rail stays up.
* Confirmed on hardware: reseat -> module answers -> one fix -> first standby
* -> gone again.
*
* 0x28 sets both, so a board that does wire EXTINT keeps that path as well.
* Module stops all output and draws ~20µA (ZOE-M8Q at 3V, datasheet Table 13;
* the 15µA hardware-backup figure needs VCC removed entirely). */
static const uint8_t ubx_pmreq_backup[] = {
0xB5, 0x62, /* UBX sync chars */
0x02, 0x41, /* Class: RXM, ID: PMREQ */
0x10, 0x00, /* Length: 16 bytes (little-endian) */
/* Payload */
0x00, /* version */
0x00, 0x00, 0x00, /* reserved1[3] */
0x00, 0x00, 0x00, 0x00, /* duration: 0 = infinite */
0x06, 0x00, 0x00, 0x00, /* flags: backup(0x02) | force(0x04) */
0x28, 0x00, 0x00, 0x00, /* wakeupSources: uartrx(bit3) | extint0(bit5) */
/* Checksum (Fletcher-8 over class..payload) */
0x81, 0xEB
};
/* Put GPS module into software sleep (for boards without GPIO power control).
* Sends both Quectel PMTK and u-blox UBX commands — the wrong one is
* harmlessly ignored by whichever module is actually connected. */
static void gps_software_sleep(void)
{
LOG_INF("GPS: Sending software sleep (PMTK + UBX)");
/* MediaTek standby */
gps_uart_send(pmtk_standby, sizeof(pmtk_standby) - 1); /* exclude null terminator */
/* Small delay between commands — let the first one drain */
k_msleep(50);
/* u-blox ZOE-M8Q backup */
gps_uart_send(ubx_pmreq_backup, sizeof(ubx_pmreq_backup));
LOG_DBG("GPS: Software sleep commands sent");
}
/* Wake GPS module from software sleep.
* A single 0xFF byte on UART triggers wake on both Quectel and u-blox.
* After wake, the module resumes NMEA output within ~100-500ms. */
static void gps_software_wake(void)
{
LOG_INF("GPS: Sending UART wake byte");
const uint8_t wake = 0xFF;
gps_uart_send(&wake, 1);
/* Give the module time to boot and start NMEA output */
k_msleep(200);
}
#endif /* HAS_GPS_UART && !HAS_GPS_POWER_CONTROL && !HAS_GPS_POWER_REGULATOR */
/* Module on or off with whatever this board has: the power GPIO or PMU rail,
* else the UART sleep commands. keep_vrtc: T1000-E warm standby (off only). */
uint32_t gps_power_on_count;
uint32_t gps_power_off_count;
void gps_module_power(bool on, bool keep_vrtc)
{
if (on) {
gps_power_on_count++;
} else {
gps_power_off_count++;
}
#if HAS_GPS_POWER_CONTROL || HAS_GPS_POWER_REGULATOR
gps_power_control(on, keep_vrtc);
#elif HAS_GPS_UART
ARG_UNUSED(keep_vrtc);
if (on) {
gps_software_wake();
} else {
gps_software_sleep();
}
#else
ARG_UNUSED(on);
ARG_UNUSED(keep_vrtc);
#endif
}
#if HAS_GPS_POWER_CONTROL
/**
* Log actual GPIO pin states after power-up sequence.
* Reads back each configured pin to verify the hardware accepted our config.
*/
void gps_dump_gpio_states(void)
{
/* Port/pin come from the gpio_dt_spec so the board's real wiring is
* printed. These used to be hardcoded T1000-E pin numbers, which read
* as plausible nonsense on every other board. */
#define GPS_LOG_PIN(_label, _spec) \
if (gpio_is_ready_dt(&(_spec))) { \
LOG_INF(" %-14s %s.%02u: %d", _label, (_spec).port->name, \
(_spec).pin, gpio_pin_get_dt(&(_spec))); \
}
LOG_INF("GPS GPIO states after power-up:");
GPS_LOG_PIN("GPS_EN", gps_enable_gpio);
#if HAS_GPS_VRTC
GPS_LOG_PIN("GPS_VRTC_EN", gps_vrtc_gpio);
#endif
#if HAS_GPS_RESET
GPS_LOG_PIN("GPS_RESET", gps_reset_gpio);
#endif
#if HAS_GPS_SLEEP
GPS_LOG_PIN("GPS_SLEEP_INT", gps_sleep_gpio);
#endif
#if HAS_GPS_RTCINT
GPS_LOG_PIN("GPS_RTC_INT", gps_rtcint_gpio);
#endif
#if HAS_GPS_RESETB
GPS_LOG_PIN("GPS_RESETB", gps_resetb_gpio); /* INPUT_PULLUP, expect 0 (logical, de-asserted) */
#endif
#undef GPS_LOG_PIN
}
#endif /* HAS_GPS_POWER_CONTROL */
#endif /* HAS_GNSS */