gps fiddling

This commit is contained in:
liquidraver
2026-08-04 16:04:05 +02:00
parent 788e4dd48b
commit f83ce0bed4
21 changed files with 1988 additions and 224 deletions
+1
View File
@@ -124,6 +124,7 @@ UI_AUDIT_INDEX.md
RADIO2_AUDIT_INDEX.md
MISC_AUDIT_INDEX.md
HANDOVER_issue34_ble_esp32.md
HANDOVER_crypto_hw_accel.md
PATH_HASH_AUDIT_INDEX.md
MESHTIMESYNC_PLAN.md
/meshtimesync-sim/
+44
View File
@@ -764,6 +764,50 @@ config ZEPHCORE_REPEATER_GPS_INTERVAL_SEC
overrides it with "set gps duty <sec>" (persisted to flash).
Companions default to ZEPHCORE_GPS_POLL_INTERVAL_SEC (300s) instead.
config ZEPHCORE_GPS_NAV_MODE
int "GNSS navigation dynamic model (CASIC $PCAS11)"
range -1 7
default 1 if ZEPHCORE_ROLE_REPEATER || ZEPHCORE_ROLE_ROOM_SERVER
default 3
help
Dynamic model sent to CASIC-family GNSS modules (Quectel L76K/L76KB,
Air530Z) on boards using the generic-NMEA driver: 0 = portable,
1 = stationary, 2 = pedestrian, 3 = automotive, 4 = sea,
5-7 = airborne. -1 sends nothing.
Defaults to stationary for fixed roles and automotive otherwise.
Worth setting rather than leaving alone: the model is stored IN THE
MODULE and survives reflashing the host, so a slot module that
previously lived in another device can arrive stuck in an airborne
model that quietly degrades fixes on a fixed site.
Boards that always carry a CASIC part use the air530z driver instead
— see CONFIG_GNSS_LUATOS_AIR530Z_NAV_MODE for those.
config ZEPHCORE_GPS_SAT_DIAG
bool "GPS per-constellation satellite diagnostics"
select GNSS_SATELLITES
default y if ZEPHCORE_ROLE_REPEATER
help
Tally tracked satellites per constellation from GSV sentences and
report them in "get gps" as sys=G<n>/R<n>/E<n>/B<n>/?<n>
(GPS / GLONASS / Galileo / BeiDou / other).
This is the ground truth for whether the boot-time multi-constellation
configuration (PMTK353 / UBX-CFG-GNSS) actually took: a module still in
its GPS-only default emits only $GPGSV, so every counter but G stays 0.
Satellite count alone cannot prove this — only the talker IDs can.
Costs RAM: CONFIG_GNSS_SATELLITES adds 512 bytes to the driver's UART
RX buffer plus a per-satellite array (~770 bytes total, measured on
RAK3401). Default on for repeaters, which have the headroom; off for
companions, which are RAM-bound. Enable it explicitly for a companion
diagnostic build.
Independent of "set gps diag", which is a runtime toggle and always
available — this option only adds the per-constellation evidence to
its report.
endmenu
menu "WiFi OTA Update"
+32
View File
@@ -133,6 +133,37 @@ Regions control which flood packets the repeater forwards. The region tree is hi
| `gps advert prefs` | Include stored lat/lon from prefs in advertisements |
| `set gps duty <sec>` | GPS duty interval (standby seconds between fixes). `0` = always-on (continuous; streams fresh fixes, can download a full almanac). Floor 10s, cap 604800 (1 week). Persists to flash, applied live. |
| `set gps duty default` | Reset GPS duty to the role default (repeater/room 48h, companion 300s) |
| `set gps diag <0\|1\|on\|off>` | Arm GPS module-configuration diagnostics (see below). Not persisted — clears on reboot |
**GPS configuration diagnostics.** At boot the firmware configures the GNSS module — constellations, AssistNow/EASY, minimum elevation, fix rate — and on modules driven over raw NMEA those commands are sent **blind**: nothing reads the module's reply, so a silently rejected configuration is indistinguishable from a working one. These two commands make that visible.
```
set gps diag 1 # arm it
gps off # power-cycle the module...
gps on # ...which re-runs configuration and records the result
get gps diag # read it back
```
Sample reply:
```
> diag=on cfg=uart age=910s mod=URANUS5 sent=12/336B sys=G3/R4/E0/B3/?0
```
- `cfg=` which path ran — `uart` (raw PMTK+PCAS+UBX), `api` (driver GNSS API), `blind` (neither available), or `never-run`
- `mod=` module identification captured from the `$GPTXT` reply to a version query, or `no-reply`
- `sent=` commands/bytes written to the module (UART path), or `sys_ret=`/`rate_ret=` return codes (API path)
- `sys=` tracked satellites per constellation from GSV talker IDs: **G**PS / GLONASS (**R**) / Galileo (**E**) / **B**eiDou / other. A constellation that stops reporting for 30 s decays to zero rather than showing a stale count
`sys=` totalling more than `sats=` in `get gps` is expected, not a discrepancy: GSV counts satellites **tracked**, GGA counts satellites **used in the fix solution**.
**`mod=` is the TX-path proof.** Everything else on this transport is written blind, so a module that hears nothing looks exactly like one that hears everything and ignores it. A version string means the module received a command and answered. `mod=no-reply` alongside a healthy `sats=` in `get gps` means the receive direction works but the module is not hearing us — wiring or pin assignment, not configuration.
**`sent=` proves transmission, not acceptance.** Only `sys=` shows what the module actually did. A module still running its factory or previously saved configuration reports `G` non-zero with the rest at `0`. Note `B0` is expected on u-blox M8 (BeiDou is deliberately disabled — only three major constellations can run concurrently), and `?0` is normal outside Japan (QZSS is regional).
The generic-NMEA path sends three protocols — PMTK (MediaTek), PCAS (CASIC: Quectel L76K/L76KB, Air530Z) and UBX (u-blox) — because a WisBlock-style GPS slot can hold any of them and each family ignores what it does not understand. Related build option: `CONFIG_ZEPHCORE_GPS_NAV_MODE` sets the CASIC navigation dynamic model (`$PCAS11`), defaulting to stationary for repeaters and room servers and automotive otherwise. It is worth setting because that model is stored *in the module* and survives reflashing the host — a slot module that previously lived in another device can arrive stuck in an airborne model that quietly degrades fixes on a fixed site.
Caveats: the `sys=` tally needs `CONFIG_ZEPHCORE_GPS_SAT_DIAG` (default on for repeaters, off for companions to save RAM) — the reply says so when built without it. Only the raw-UART path is re-run on `gps on`; boards with a real GNSS driver (Air530Z, LC76G) keep reporting their boot-time result, because that path goes through `modem_chat_run_script()`, which is safe only at boot. On those boards `E0` is also expected — the Air530Z driver supports GPS/GLONASS/BeiDou but not Galileo, and the firmware falls back automatically.
---
@@ -216,6 +247,7 @@ All `set uplink.*` changes are saved immediately and only applied after reboot.
| `get radio.rxgain` | RX gain boost: `0` or `1` |
| `get rxduty` | RX duty cycle mode: `0` or `1` |
| `get gps duty` | Now-effective GPS duty interval in seconds (`always on (0)` when continuous) |
| `get gps diag` | What the last GPS module-configuration attempt did — which path ran, bytes sent, and tracked satellites per constellation. See **GPS configuration diagnostics** in the GPS section for the field reference |
| `get meshtimesync` | Mesh time-sync state + live dry-run: on/off, eligible voter count, votes for/against, consensus skew and radius, would-be verdict (`ok`/`in-band`/`step±N`/`abstain (reason)`/`hold (reason)`; a recent clock set — manual or GPS — shows as `hold (suppressed)`, and a backward step a forward-only role would refuse is annotated `(skipped: forward-only)`), step counters, suppression countdown, and a per-sender evidence table (`prefix hops count skew E`, `E` = counted toward the verdict above). Entries that count print first, so a size-capped reply never hides the ones that explain the summary; if the table doesn't fully fit, a trailing `+N more` shows how many were left out. Sensing runs even while off, so this works as a dry-run before enabling. Over remote admin the reply is truncated to the packet size (summary always fits); the full table needs the USB CLI. |
| `get probe.interval` | Seconds between periodic radio measurements (noise-floor sample + CAD probe). 0 = CAD probing off |
| `get dc.restarts` | Duty-cycle preamble false-positive re-arm counter (RxTimeout re-arms + parked-RX watchdog recoveries). High values mean the preamble detector is tripping on noise/interference without real packets arriving — inflates RX-on time and drains battery; packets are never lost to it. Reset by `clear stats`. |
+610 -69
View File
@@ -48,11 +48,51 @@ LOG_MODULE_REGISTER(zephcore_gps, CONFIG_ZEPHCORE_GPS_LOG_LEVEL);
#define HAS_GNSS 0
#endif
/* ========== GPS Feature Detection ==========
* Every HAS_GPS_* predicate is defined HERE, before first use. They are pure
* devicetree tests with no side effects, kept apart from the variables they
* gate so that ordering can never drift again.
*
* Why this block exists: HAS_GPS_UART used to be defined ~500 lines below its
* first `#if`, and an undefined identifier in `#if` is silently 0 — so the
* entire PMTK/UBX module-configuration path compiled to nothing on every
* board, and GPS ran at module defaults (GPS-only constellations, no AOP).
* If you add another HAS_GPS_* macro, define it in this block.
*/
/* GNSS module hangs off a UART we can write to (any compatible). */
#if HAS_GNSS && DT_NODE_HAS_STATUS(DT_NODELABEL(gnss), okay) && \
DT_NODE_HAS_STATUS(DT_BUS(DT_NODELABEL(gnss)), okay)
#define HAS_GPS_UART 1
#else
#define HAS_GPS_UART 0
#endif
/* Discrete GPS power-enable GPIO (gps-enable alias). */
#if DT_NODE_EXISTS(DT_ALIAS(gps_enable))
#define HAS_GPS_POWER_CONTROL 1
#else
#define HAS_GPS_POWER_CONTROL 0
#endif
/* GPS powered from a PMU regulator rail (chosen zephcore,gps-power). */
#if DT_NODE_EXISTS(DT_CHOSEN(zephcore_gps_power))
#define HAS_GPS_POWER_REGULATOR 1
#else
#define HAS_GPS_POWER_REGULATOR 0
#endif
/* ========== GPS Power Strategy ==========
* Module power is GPIO/regulator controlled — GNSS driver PM is not used
* for the module itself:
* - Wio Tracker L1 (L76K): FORCE_ON pin LOW = hardware standby (~360µA,
* Vcc stays on, ephemeris/almanac/RTC preserved, hot-start 1-2s)
* - 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 LOW + VRTC HIGH = warm standby (ephemeris
* preserved via backup RAM, ~1-2µA VRTC current)
* - All boards: gps-enable alias → GPIO power control
@@ -342,6 +382,68 @@ static void gnss_data_cb(const struct device *dev, const struct gnss_data *data)
/* Register GNSS callback for all GNSS devices */
GNSS_DATA_CALLBACK_DEFINE(NULL, gnss_data_cb);
#ifdef CONFIG_ZEPHCORE_GPS_SAT_DIAG
/* ========== Per-constellation satellite tally (diagnostic) ==========
* The Zephyr GSV parser fills gnss_satellite.system from the NMEA talker ID
* ($GPGSV/$GLGSV/$GAGSV/$GBGSV), so this is direct evidence of which
* constellations the module is actually tracking — the only way to confirm
* that the boot-time PMTK353 / UBX-CFG-GNSS configuration was accepted.
* A module still in its GPS-only default yields sats_gps only.
*
* Counts only tracked satellites (is_tracked), not merely visible ones. */
static uint8_t sat_count[5]; /* gps, glonass, galileo, beidou, other */
static int64_t sat_seen_ms[5]; /* uptime when each bucket was last reported */
/* A constellation absent for this long is reported as zero. Long enough to
* ride out a missed GSV cycle (they repeat at the fix rate), short enough
* that a constellation which genuinely drops out stops being claimed. */
#define SAT_TALLY_STALE_MS 30000
static void gnss_satellites_cb(const struct device *dev,
const struct gnss_satellite *satellites,
uint16_t size)
{
ARG_UNUSED(dev);
uint8_t tally[5] = { 0 };
bool seen[5] = { false };
for (uint16_t i = 0; i < size; i++) {
int idx;
switch (satellites[i].system) {
case GNSS_SYSTEM_GPS: idx = 0; break;
case GNSS_SYSTEM_GLONASS: idx = 1; break;
case GNSS_SYSTEM_GALILEO: idx = 2; break;
case GNSS_SYSTEM_BEIDOU: idx = 3; break;
default: idx = 4; break;
}
/* Mark the constellation as reported even when nothing in it is
* tracked — that is a real "zero", distinct from "not heard". */
seen[idx] = true;
if (satellites[i].is_tracked) {
tally[idx]++;
}
}
/* One GSV burst carries ONE constellation: the parser publishes each
* talker's group separately (satellites_length == number_of_svs for
* that group). So update only the buckets this burst reported —
* replacing all five wholesale wipes the constellations that arrived
* in the previous burst, which reads as G0 next to a healthy fix. */
k_mutex_lock(&gps_mutex, K_FOREVER);
for (int i = 0; i < 5; i++) {
if (seen[i]) {
sat_count[i] = tally[i];
sat_seen_ms[i] = k_uptime_get();
}
}
k_mutex_unlock(&gps_mutex);
}
GNSS_SATELLITES_CALLBACK_DEFINE(NULL, gnss_satellites_cb);
#endif /* CONFIG_ZEPHCORE_GPS_SAT_DIAG */
/* Find and initialize GNSS device */
static const struct device *gnss_dev = NULL;
@@ -358,6 +460,43 @@ static const struct device *gnss_dev = NULL;
* persist). So we only need to configure once. */
static bool gnss_configured = false;
/* ========== Configuration Diagnostics ==========
* Module configuration is sent blind — nothing in the protocol path tells us
* the module accepted it. This records what was attempted so the operator can
* read it back over the CLI on a release build. `gps_set_diag(true)` also
* clears gnss_configured, so the next GPS enable re-runs configuration and
* refreshes the record. RAM only, never persisted. */
enum gps_cfg_path {
GPS_CFG_NEVER = 0, /* configuration has not run yet */
GPS_CFG_API, /* driver implements the GNSS API (air530z, lc76g, ...) */
GPS_CFG_UART, /* passive NMEA listener — raw PMTK + UBX sent */
GPS_CFG_BLIND, /* no GNSS API and no writable UART — module defaults */
};
static struct {
uint8_t path; /* enum gps_cfg_path */
int8_t api_ret; /* gnss_set_enabled_systems() result */
int8_t rate_ret; /* gnss_set_fix_rate() result */
uint8_t cmds; /* config commands written to the UART */
uint16_t bytes; /* bytes written to the UART */
int64_t at_ms; /* uptime when configuration last ran */
} gps_cfg_diag;
static bool gps_diag_on = false;
/* Module identification string, captured by the GNSS driver from the reply to
* its version query (CASIC parts answer in-band as a $GPTXT sentence).
* Weak so that boards whose driver has no version query still link — an
* absent symbol and an empty string mean the same thing to the report.
*
* Its real value is not the version text: on a transport where every command
* is written blind, a captured reply is the only positive proof that the
* MCU's TX line reaches the module at all. */
extern "C" int zephcore_gnss_version_get(char *buf, size_t len) __attribute__((weak));
/* True only while gps_configure_via_uart() is running (see gps_uart_send). */
static bool gps_cfg_counting = false;
/* ========== Vendor-Specific Configuration Commands ==========
*
* The RAK WisBlock GPS slot accepts multiple modules (L76K, ZOE-M8Q, etc.)
@@ -372,7 +511,43 @@ static bool gnss_configured = false;
#if HAS_GPS_UART
/* --- Quectel L76K (PMTK) configuration --- */
/* The UART the GNSS module is connected to. Works for any GNSS-on-UART node
* regardless of compatible string. */
static const struct device *gps_uart_dev = DEVICE_DT_GET(DT_BUS(DT_NODELABEL(gnss)));
/* Send raw bytes to the GPS UART using blocking poll_out.
* Safe to call even though modem_chat/modem_ubx owns the UART pipe:
* uart_poll_out writes one byte at a time through the TX register,
* and GNSS modules are receive-only (no TX contention).
*
* Gated on HAS_GPS_UART alone — writing to the module is safe on every
* UART-attached GNSS. The narrower power-control gate below applies to the
* software *sleep* commands, which are only a fallback for boards that
* cannot cut GPS power. */
static void gps_uart_send(const uint8_t *data, size_t len)
{
if (!device_is_ready(gps_uart_dev)) {
return;
}
for (size_t i = 0; i < len; i++) {
uart_poll_out(gps_uart_dev, data[i]);
}
/* Count only configuration traffic — the same helper carries the
* sleep/wake commands, which would otherwise inflate the tally. */
if (gps_cfg_counting) {
gps_cfg_diag.cmds++;
gps_cfg_diag.bytes += (uint16_t)len;
}
}
/* --- MediaTek-family (PMTK) configuration ---
*
* PMTK is MediaTek's protocol. It applies to genuine MTK parts (L76B and
* relatives). It does NOT apply to the Quectel L76K/L76KB or Air530Z, which
* are CASIC silicon and speak PCAS — their protocol specification contains no
* PMTK command at all, so these sentences are inert there. Those modules are
* driven by the air530z driver via the GNSS API instead and never reach this
* path. Kept because the RAK WisBlock GPS slot can hold an MTK part. */
/* PMTK353: Enable GPS + GLONASS + Galileo + BeiDou (no QZSS).
* Default is GPS-only. Multi-constellation dramatically improves TTFF
@@ -391,20 +566,172 @@ static const char pmtk_easy[] = "$PMTK869,1,1*35\r\n";
* Especially useful when GPS antenna is near the SX1262 + SKY66122 PA. */
static const char pmtk_aic[] = "$PMTK286,1*23\r\n";
/* --- CASIC (PCAS) configuration ---
*
* The Quectel L76K/L76KB (RAK12501) and Air530Z are CASIC silicon: they speak
* neither PMTK nor UBX, so without these sentences such a module in a
* WisBlock slot receives no configuration at all and sits on its factory
* defaults. Boards that always carry one use the air530z driver and the GNSS
* API instead; these are for the generic-NMEA boards whose GPS slot can hold
* any module.
*
* Inert on the other families, same as PMTK and UBX are here. */
/* PCAS03: NMEA sentence selection. Field order is
* GGA,GLL,GSA,GSV,RMC,VTG,ZDA,ANT,... — keep GGA + RMC (position, time) and
* add GSV only when the satellite tally needs it, to keep the 9600-baud link
* from spending its budget on sentences nobody parses. */
#ifdef CONFIG_ZEPHCORE_GPS_SAT_DIAG
static const char pcas_sentences[] = "$PCAS03,1,0,0,1,1,0,0,0,0,0,0,0,0*1F\r\n";
#else
static const char pcas_sentences[] = "$PCAS03,1,0,0,0,1,0,0,0,0,0,0,0,0*1E\r\n";
#endif
/* PCAS04,7 = GPS + BeiDou + GLONASS, everything the part supports.
* (No Galileo on these modules — that is silicon, not configuration.) */
static const char pcas_constellations[] = "$PCAS04,7*1E\r\n";
/* PCAS11: navigation dynamic model. Stored IN THE MODULE and survives
* reflashing the host, so a slot module that previously lived in another
* device can arrive stuck in an automotive or airborne model that quietly
* degrades fixes on a fixed site. See CONFIG_ZEPHCORE_GPS_NAV_MODE. */
#if CONFIG_ZEPHCORE_GPS_NAV_MODE == 0
static const char pcas_nav_mode[] = "$PCAS11,0*1D\r\n";
#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 1
static const char pcas_nav_mode[] = "$PCAS11,1*1C\r\n";
#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 2
static const char pcas_nav_mode[] = "$PCAS11,2*1F\r\n";
#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 3
static const char pcas_nav_mode[] = "$PCAS11,3*1E\r\n";
#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 4
static const char pcas_nav_mode[] = "$PCAS11,4*19\r\n";
#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 5
static const char pcas_nav_mode[] = "$PCAS11,5*18\r\n";
#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 6
static const char pcas_nav_mode[] = "$PCAS11,6*1B\r\n";
#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 7
static const char pcas_nav_mode[] = "$PCAS11,7*1A\r\n";
#endif
/* PCAS06,0: ask the module to identify itself. A CASIC part answers in-band
* with a $GPTXT sentence — the only readable reply available on this
* otherwise write-only path, and therefore the only positive proof that the
* MCU's TX line reaches the module at all. Captured by the GNSS driver and
* surfaced as `mod=` in "get gps diag". */
static const char pcas_version_query[] = "$PCAS06,0*1B\r\n";
/* u-blox equivalent of the query above. u-blox ignore $PCAS06 entirely, so
* without this a u-blox module always reports "no reply" and the TX-path
* proof — the whole point of asking — is unavailable on those boards. The
* $PUBX,04 reply carries time and clock status, not a version; what matters
* is that a reply arrives at all, which only happens if the module received
* the request. */
static const char pubx_version_query[] = "$PUBX,04*37\r\n";
/* --- u-blox NMEA output trim ($PUBX,40) ---
*
* THE LINK BUDGET IS THE CONSTRAINT, and it is easy to blow past it. At 9600
* baud only ~960 bytes/s fit. With multi-GNSS enabled a u-blox emits, per
* second: GGA + RMC + GLL + VTG, one GSA per constellation, and a GSV burst
* that grows with satellite count — roughly 1030 bytes/s at ~26 SVs. The
* stream then cannot fit in the second it is generated in, sentences are
* truncated or dropped, and the symptom is not "slow GPS" but a receiver
* that appears to have stopped: no parseable GGA, so no fix, so no position.
*
* Enabling constellations without trimming output is therefore actively
* harmful on a 9600-baud link. We already do exactly this trim for CASIC
* parts via $PCAS03; u-blox had no equivalent, which is the asymmetry this
* fixes. GLL, GSA and VTG are dropped outright — nothing in the driver parses
* them — and GSV is kept only when the satellite tally needs it.
*
* Dropping GLL/GSA/VTG takes ~1030 -> ~650 bytes/s; dropping GSV as well
* takes it to ~160. */
static const char pubx_off_gll[] = "$PUBX,40,GLL,0,0,0,0,0,0*5C\r\n";
static const char pubx_off_gsa[] = "$PUBX,40,GSA,0,0,0,0,0,0*4E\r\n";
static const char pubx_off_vtg[] = "$PUBX,40,VTG,0,0,0,0,0,0*5E\r\n";
#ifndef CONFIG_ZEPHCORE_GPS_SAT_DIAG
static const char pubx_off_gsv[] = "$PUBX,40,GSV,0,0,0,0,0,0*59\r\n";
#endif
/* --- u-blox ZOE-M8Q (UBX binary) configuration --- */
/* UBX-CFG-GNSS: Enable GPS + Galileo + BeiDou + GLONASS.
* ZOE-M8Q defaults to GPS-only. Multi-constellation dramatically improves
* TTFF and fix reliability — more visible satellites in any sky condition.
* 32 tracking channels allocated across 4 active systems.
* SBAS disabled — needs 30-60s to download corrections, useless for our
* quick-fix-then-sleep pattern (companions: 30s, repeaters: 5min).
* QZSS disabled — Japan regional, wastes tracking channels elsewhere. */
/* UBX-CFG-PRT: force UART1 to 9600 8N1 with BOTH UBX and NMEA enabled in and
* out. Sent first, before anything that depends on the module talking to us.
*
* This exists because the module's port configuration is persistent and not
* necessarily ours. RAK's own RAK12500 example — and any host using the
* SparkFun u-blox library — calls setUART1Output(COM_TYPE_UBX) followed by
* saveConfiguration(), which stores "UBX only, NMEA off" in the module's
* flash. A module that has ever been driven that way stays silent on an
* NMEA-only host forever after, through power cycles and reflashes, and
* presents as a completely dead receiver: no GGA, no fix, no reply to any
* query. Re-asserting the port configuration costs one frame and removes a
* failure mode that is otherwise almost impossible to diagnose from the host.
*
* Limitation: if the module was also saved at a different baud rate, it will
* not parse this frame either. Recovering from that needs a baud scan, which
* the devicetree's fixed current-speed does not currently allow. */
static const uint8_t ubx_cfg_prt_uart1[] = {
0xB5, 0x62, 0x06, 0x00, 0x14, 0x00, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x08,
0x00, 0x00, 0x80, 0x25, 0x00, 0x00, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00,
0x00, 0x00, 0x8E, 0x95
};
/* UBX-CFG-GNSS: Enable GPS + Galileo + GLONASS (+ QZSS) on u-blox M8.
*
* Config block layout is gnssId, resTrkCh, maxTrkCh, reserved0, flags[4] —
* EIGHT bytes, and the payload length must be exactly 4 + 8*numConfigBlocks
* or the receiver rejects the whole message. The previous version of this
* frame omitted reserved0, giving 7-byte blocks and a 39-byte payload where
* numConfigBlocks=5 demanded 44, so no u-blox module ever accepted it.
*
* Only THREE major GNSS (GPS/Galileo/GLONASS/BeiDou) can run concurrently on
* M8, so BeiDou is explicitly disabled rather than left alone: if the module
* came up with BeiDou on, enabling three others would make four and the
* message would be refused.
*
* QZSS is enabled even though it is Japan-regional and costs ~3 channels —
* u-blox require GPS and QZSS to be both enabled or both disabled (they
* share L1 C/A), and a mismatch is grounds for rejection.
*
* SBAS stays off: it needs 30-60 s to download corrections, useless for our
* quick-fix-then-sleep pattern (companions 30 s, repeaters 5 min).
*
* numTrkChHw = 0 and numTrkChUse = 0xFF (read-only / "use max available").
*
* resTrkCh MUST be 0 on the disabled blocks. Reserving tracking channels for
* a system whose enable bit is clear is self-contradictory, and the receiver
* validates CFG-GNSS atomically — one bad block rejects all six. An earlier
* revision left SBAS at 1 and BeiDou at 8 and the whole frame was refused
* (observed on hardware: RAK12500/ZOE-M8Q stayed GPS-only, sys=G8/R0/E0/B0).
*
* NOTE — this is an M8 frame. u-blox 7 parts (MAX-7Q on the RAK1910) have no
* Galileo or BeiDou and use a different sigCfgMask, so they will refuse it
* and stay GPS-only. Sending an M7 frame as well is NOT safe blind: its
* sigCfgMask of 0 would be a signal-disabling value on M8. */
static const uint8_t ubx_cfg_gnss[] = {
0xB5, 0x62, 0x06, 0x3E, 0x27, 0x00, 0x00, 0x20, 0x20, 0x05, 0x00, 0x08,
0x10, 0x01, 0x00, 0x01, 0x00, 0x02, 0x04, 0x0A, 0x01, 0x00, 0x01, 0x00,
0x03, 0x04, 0x0A, 0x01, 0x00, 0x01, 0x00, 0x05, 0x00, 0x03, 0x00, 0x00,
0x01, 0x00, 0x06, 0x04, 0x0A, 0x01, 0x00, 0x01, 0x00, 0x0E, 0x13
0xB5, 0x62, 0x06, 0x3E, 0x34, 0x00, 0x00, 0x00, 0xFF, 0x06, 0x00, 0x08,
0x10, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x03, 0x00, 0x00, 0x00,
0x01, 0x01, 0x02, 0x04, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01, 0x03, 0x00,
0x10, 0x00, 0x00, 0x00, 0x01, 0x01, 0x05, 0x00, 0x03, 0x00, 0x01, 0x00,
0x01, 0x01, 0x06, 0x08, 0x0E, 0x00, 0x01, 0x00, 0x01, 0x01, 0xEE, 0x64
};
/* UBX-CFG-NMEA: switch the NMEA output to version 4.10.
*
* Without this, Galileo and BeiDou satellites cannot be reported at all: the
* $GAGSV and $GBGSV talker IDs only exist from NMEA 4.10, and M8 firmware
* defaults lower. So a fully successful CFG-GNSS would still show zero
* Galileo in "get gps diag" — a reporting limit masquerading as a config
* failure. Mirrors Meshtastic's "enable NMEA 4.10" step (src/gps/ubx.h).
*
* gsvTalkerId = 0 (use the GNSS-specific talker per constellation) is what
* makes the per-constellation tally work — do not set it to 1, which forces
* every GSV onto the main talker and would collapse the tally into GPS. */
static const uint8_t ubx_cfg_nmea_410[] = {
0xB5, 0x62, 0x06, 0x17, 0x14, 0x00, 0x00, 0x41, 0x00, 0x02, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x75, 0x57
};
/* UBX-CFG-NAV5: Set 5° minimum satellite elevation.
@@ -413,12 +740,15 @@ static const uint8_t ubx_cfg_gnss[] = {
* everything including horizon-level junk.
* Dynamic model left at factory default (Portable) — works for fixed
* repeaters, walking companions, and vehicles alike.
* apply mask 0x0002 = minEl(bit1) only */
* apply mask 0x0002 = minEl(bit1) only
*
* The trailing checksum was CK_B=0x37 (should be 0xE7) — a one-byte typo
* that made every receiver drop this frame silently, with no NAK. */
static const uint8_t ubx_cfg_nav5_minelev[] = {
0xB5, 0x62, 0x06, 0x24, 0x24, 0x00, 0x02, 0x00, 0x00, 0x03, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x58, 0x37
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x58, 0xE7
};
/* UBX-CFG-NAVX5: Enable AssistNow Autonomous (AOP).
@@ -443,20 +773,73 @@ static const uint8_t ubx_cfg_save[] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x17, 0x31, 0xBF
};
/* Send a PMTK command string (including \r\n). Adds small delay after. */
static void gps_send_pmtk(const char *cmd)
/* Settle time after a command that makes the receiver restart its navigation
* engine — constellation changes on every family do this. A command issued
* into a restarting engine is simply lost, and because everything here is
* written blind the loss is silent. 20-50 ms was optimistic. */
#define GPS_CFG_SETTLE_MS 120
#define GPS_CFG_RESTART_MS 500
/* Send an NMEA command string (including \r\n). */
static void gps_send_nmea(const char *cmd, uint32_t settle_ms = GPS_CFG_SETTLE_MS)
{
gps_uart_send((const uint8_t *)cmd, strlen(cmd));
k_msleep(20); /* Let module process before next command */
k_msleep(settle_ms);
}
/* Send a UBX binary frame. Adds small delay after for processing. */
static void gps_send_ubx(const uint8_t *frame, size_t len)
/* Send a UBX binary frame. */
static void gps_send_ubx(const uint8_t *frame, size_t len,
uint32_t settle_ms = GPS_CFG_SETTLE_MS)
{
gps_uart_send(frame, len);
k_msleep(50); /* UBX needs more time to ACK + apply config */
k_msleep(settle_ms);
}
/* u-blox dynamic model for CFG-NAV5, translated from the CASIC-numbered
* CONFIG_ZEPHCORE_GPS_NAV_MODE so one setting drives both families.
* u-blox: 0 portable, 2 stationary, 3 pedestrian, 4 automotive, 5 sea,
* 6-8 airborne. */
#if CONFIG_ZEPHCORE_GPS_NAV_MODE == 1
#define UBX_DYNMODEL 2
#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 2
#define UBX_DYNMODEL 3
#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 3
#define UBX_DYNMODEL 4
#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 4
#define UBX_DYNMODEL 5
#elif CONFIG_ZEPHCORE_GPS_NAV_MODE >= 5
#define UBX_DYNMODEL 6
#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 0
#define UBX_DYNMODEL 0
#endif
/* Send CFG-NAV5 with the dynamic model patched in and the checksum redone.
* A stationary model on a fixed node suppresses position wander and lets the
* receiver apply much tighter velocity assumptions; leaving a roof-mounted
* repeater on the factory Portable model throws that away. Patched at runtime
* rather than as eight hard-coded frames — one place to get the checksum
* right instead of eight. */
#ifdef UBX_DYNMODEL
static void gps_send_ubx_nav5(void)
{
uint8_t f[sizeof(ubx_cfg_nav5_minelev)];
memcpy(f, ubx_cfg_nav5_minelev, sizeof(f));
f[6] = 0x03; /* mask: dyn (bit0) + minEl (bit1) */
f[8] = UBX_DYNMODEL; /* dynModel */
uint8_t ck_a = 0, ck_b = 0;
for (size_t i = 2; i < sizeof(f) - 2; i++) {
ck_a = (uint8_t)(ck_a + f[i]);
ck_b = (uint8_t)(ck_b + ck_a);
}
f[sizeof(f) - 2] = ck_a;
f[sizeof(f) - 1] = ck_b;
gps_send_ubx(f, sizeof(f));
}
#endif
/* Configure the GPS module with optimal settings for a mesh repeater.
* Sends both PMTK (Quectel) and UBX (u-blox) commands — the module that
* isn't present ignores bytes it doesn't understand. */
@@ -464,21 +847,91 @@ static void gps_configure_via_uart(void)
{
LOG_INF("GPS: Configuring via UART (PMTK + UBX dual-protocol)");
/* --- Quectel L76K (PMTK) --- */
gps_send_pmtk(pmtk_constellations);
gps_send_pmtk(pmtk_easy);
gps_send_pmtk(pmtk_aic);
gps_cfg_diag.cmds = 0;
gps_cfg_diag.bytes = 0;
gps_cfg_counting = true;
/* --- MediaTek-family (PMTK) --- */
gps_send_nmea(pmtk_constellations, GPS_CFG_RESTART_MS);
gps_send_nmea(pmtk_easy);
gps_send_nmea(pmtk_aic);
LOG_INF("GPS: PMTK config sent (constellations, EASY, AIC)");
/* --- u-blox ZOE-M8Q (UBX) --- */
gps_send_ubx(ubx_cfg_gnss, sizeof(ubx_cfg_gnss));
/* --- CASIC (PCAS) ---
* Version query last, so its $GPTXT reply is not stepped on by a
* constellation restart. */
gps_send_nmea(pcas_sentences);
gps_send_nmea(pcas_constellations, GPS_CFG_RESTART_MS);
#if CONFIG_ZEPHCORE_GPS_NAV_MODE >= 0
gps_send_nmea(pcas_nav_mode);
#endif
gps_send_nmea(pcas_version_query);
LOG_INF("GPS: PCAS config sent (sentences, constellations, nav mode, version query)");
/* --- u-blox ZOE-M8Q (UBX) ---
* NMEA 4.10 goes first: it governs whether the constellations enabled
* by the next frame can be *reported* at all. CFG-GNSS restarts the
* navigation engine, so it gets the long settle before the frames that
* follow it — at 50 ms they were being issued into a restarting
* receiver. CFG-CFG stays last so it only persists whatever was
* actually accepted; note that makes a rejected configuration sticky
* too, which is why a frame bug here survives power cycles. */
/* Make sure NMEA output is even switched on before anything else — a
* module saved as UBX-only by a previous host is otherwise mute. */
gps_send_ubx(ubx_cfg_prt_uart1, sizeof(ubx_cfg_prt_uart1), GPS_CFG_RESTART_MS);
/* Trim the NMEA stream BEFORE enabling more constellations — the extra
* GSV traffic must have somewhere to fit. */
gps_send_nmea(pubx_off_gll);
gps_send_nmea(pubx_off_gsa);
gps_send_nmea(pubx_off_vtg);
#ifndef CONFIG_ZEPHCORE_GPS_SAT_DIAG
gps_send_nmea(pubx_off_gsv);
#endif
gps_send_ubx(ubx_cfg_nmea_410, sizeof(ubx_cfg_nmea_410));
gps_send_ubx(ubx_cfg_gnss, sizeof(ubx_cfg_gnss), GPS_CFG_RESTART_MS);
#ifdef UBX_DYNMODEL
gps_send_ubx_nav5();
#else
gps_send_ubx(ubx_cfg_nav5_minelev, sizeof(ubx_cfg_nav5_minelev));
#endif
gps_send_ubx(ubx_cfg_navx5_aop, sizeof(ubx_cfg_navx5_aop));
gps_send_ubx(ubx_cfg_save, sizeof(ubx_cfg_save));
LOG_INF("GPS: UBX config sent (multi-GNSS, 5° min elev, AOP, saved to flash)");
/* Ask a u-blox to say something back — the TX-path proof for this
* family, sent last so the reply is not stepped on by a restart. */
gps_send_nmea(pubx_version_query);
LOG_INF("GPS: UBX config sent (NMEA 4.10, multi-GNSS, 5° min elev, AOP, saved)");
gps_cfg_counting = false;
}
#endif /* HAS_GPS_UART */
/* Re-run module configuration on a GPS enable, but only when diagnostics are
* armed — this is a deliberate, operator-triggered action, not a normal path.
*
* Only the raw-UART path is re-runnable. gnss_configure()'s API path goes
* through modem_chat_run_script(), which is safe at boot only: after a GPIO
* power restore the chip needs ~300 ms and calling it here deadlocks the main
* thread. So on API-driver boards this records nothing new and "get gps diag"
* keeps reporting the boot-time result, which is the honest answer. */
static void gps_diag_maybe_reconfigure(void)
{
if (!gps_diag_on || gnss_configured || gnss_dev == NULL) {
return;
}
#if HAS_GPS_UART
if (gps_cfg_diag.path == GPS_CFG_UART || gps_cfg_diag.path == GPS_CFG_NEVER) {
/* The module has just been powered; give it time to boot before
* clocking configuration at it (same ~300 ms the modem needs). */
k_msleep(300);
gps_cfg_diag.path = GPS_CFG_UART;
gps_cfg_diag.at_ms = k_uptime_get();
gps_configure_via_uart();
}
#endif
gnss_configured = true;
}
static void gnss_configure(void)
{
if (gnss_configured || gnss_dev == NULL) {
@@ -496,15 +949,21 @@ static void gnss_configure(void)
systems = GNSS_SYSTEM_GPS | GNSS_SYSTEM_GLONASS | GNSS_SYSTEM_BEIDOU;
ret = gnss_set_enabled_systems(gnss_dev, systems);
}
gps_cfg_diag.api_ret = (int8_t)ret;
gps_cfg_diag.at_ms = k_uptime_get();
if (ret == 0) {
LOG_INF("GPS: Multi-constellation enabled via GNSS API");
gps_cfg_diag.path = GPS_CFG_API;
} else if (ret == -ENOSYS || ret == -ENOTSUP) {
#if HAS_GPS_UART
/* gnss-nmea-generic is a passive listener — no GNSS API.
* Configure everything via direct UART commands instead. */
gps_cfg_diag.path = GPS_CFG_UART;
gps_configure_via_uart();
#else
LOG_INF("GPS: No GNSS API and no UART access — using module defaults");
gps_cfg_diag.path = GPS_CFG_BLIND;
#endif
} else {
LOG_WRN("GPS: Failed to set constellations: %d", ret);
@@ -514,6 +973,7 @@ static void gnss_configure(void)
/* Set 1Hz fix rate (explicit, don't rely on chip defaults) */
ret = gnss_set_fix_rate(gnss_dev, 1000);
gps_cfg_diag.rate_ret = (int8_t)ret;
if (ret == 0) {
LOG_INF("GPS: Fix rate set to 1Hz");
} else if (ret != -ENOSYS && ret != -ENOTSUP) {
@@ -538,11 +998,8 @@ static void gnss_configure(void)
* - 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 DT_NODE_EXISTS(DT_ALIAS(gps_enable))
#if HAS_GPS_POWER_CONTROL
static const struct gpio_dt_spec gps_enable_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(gps_enable), gpios);
#define HAS_GPS_POWER_CONTROL 1
#else
#define HAS_GPS_POWER_CONTROL 0
#endif
/* GPS powered by a PMU regulator rail instead of a discrete enable GPIO (e.g.
@@ -551,15 +1008,12 @@ static const struct gpio_dt_spec gps_enable_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(gps
* switch driven by enable/disable; the duty-cycle standby/wake uses software
* sleep/wake (UART) and leaves the rail up, so the regulator is only toggled on
* the (unguarded) enable/disable/boot paths — never per duty cycle. */
#if DT_NODE_EXISTS(DT_CHOSEN(zephcore_gps_power))
#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;
#define HAS_GPS_POWER_REGULATOR 1
#else
#define HAS_GPS_POWER_REGULATOR 0
#endif
/* AXP2101 backup (button-battery) charger — feeds the GPS receiver's V_BCKP
@@ -860,24 +1314,24 @@ void gps_power_off_for_shutdown(void)
* 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 Quectel and u-blox sleep commands — the module that
* Strategy: send BOTH MediaTek and u-blox sleep commands — the module that
* isn't present simply ignores the bytes it doesn't understand.
*
* - Quectel L76K (RAK1910): $PMTK161,0*28\r\n → standby (~1mA), wake on UART
* - u-blox ZOE-M8Q (RAK12500): UBX-RXM-PMREQ → backup (~7µA), wake on UART
* - 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.
*/
/* Get the UART device that the GNSS module is connected to.
* Works for any GNSS-on-UART node regardless of compatible string. */
#if DT_NODE_HAS_STATUS(DT_NODELABEL(gnss), okay) && \
DT_NODE_HAS_STATUS(DT_BUS(DT_NODELABEL(gnss)), okay)
#define HAS_GPS_UART 1
#else
#define HAS_GPS_UART 0
#endif
/* 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)
@@ -921,11 +1375,6 @@ void gps_power_off_for_shutdown(void)
#define HAS_GPS_UART_PM 0
#endif
#if HAS_GPS_UART && \
(HAS_GPS_UART_PM || (!HAS_GPS_POWER_CONTROL && !HAS_GPS_POWER_REGULATOR))
static const struct device *gps_uart_dev = DEVICE_DT_GET(DT_BUS(DT_NODELABEL(gnss)));
#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;
@@ -965,22 +1414,13 @@ static inline void gps_uart_set_power(bool on) { ARG_UNUSED(on); }
#endif
#if HAS_GPS_UART && !HAS_GPS_POWER_CONTROL && !HAS_GPS_POWER_REGULATOR
/* Send raw bytes to the GPS UART using blocking poll_out.
* Safe to call even though modem_chat/modem_ubx owns the UART pipe:
* uart_poll_out writes one byte at a time through the TX register,
* and GNSS modules are receive-only (no TX contention). */
static void gps_uart_send(const uint8_t *data, size_t len)
{
if (!device_is_ready(gps_uart_dev)) {
return;
}
for (size_t i = 0; i < len; i++) {
uart_poll_out(gps_uart_dev, data[i]);
}
}
/* 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. */
/* Quectel L76K: $PMTK161,0*28\r\n → enter standby mode
* Module stops NMEA output and draws ~1mA. Wakes on any UART RX byte. */
/* 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
@@ -1012,7 +1452,7 @@ static void gps_software_sleep(void)
{
LOG_INF("GPS: Sending software sleep (PMTK + UBX)");
/* Quectel L76K standby */
/* MediaTek standby */
gps_uart_send(pmtk_standby, sizeof(pmtk_standby) - 1); /* exclude null terminator */
/* Small delay between commands — let the first one drain */
@@ -1540,6 +1980,7 @@ void gps_enable(bool enable)
* Do NOT call modem_chat_run_script() here — the chip needs
* ~300ms to boot after GPIO power restore and calling it
* immediately deadlocks the main thread. */
gps_diag_maybe_reconfigure();
/* Bounded first-acquisition window, then the normal duty cycle —
* unless always-on (interval 0), where GPS never sleeps. */
@@ -1781,6 +2222,93 @@ void gps_request_fresh_fix(void)
#endif
}
void gps_set_diag(bool on)
{
gps_diag_on = on;
#if HAS_GNSS
if (on) {
/* Re-arm configuration so the next GPS enable ("gps off" then
* "gps on") actually re-runs it and refreshes the record. */
gnss_configured = false;
}
#endif
}
bool gps_get_diag(void)
{
return gps_diag_on;
}
void gps_get_diag_report(char *buf, size_t len)
{
#if HAS_GNSS
static const char *const path_str[] = { "never", "api", "uart", "blind" };
uint8_t path = gps_cfg_diag.path;
const char *pname = (path < ARRAY_SIZE(path_str)) ? path_str[path] : "?";
if (path == GPS_CFG_NEVER) {
snprintf(buf, len, "diag=%s cfg=never-run (enable, then 'gps off'/'gps on')",
gps_diag_on ? "on" : "off");
return;
}
uint32_t age_s = (uint32_t)((k_uptime_get() - gps_cfg_diag.at_ms) / 1000);
size_t n = (size_t)snprintf(buf, len, "diag=%s cfg=%s age=%us",
gps_diag_on ? "on" : "off", pname, age_s);
if (n >= len) {
return;
}
/* Module identity, when the driver could obtain it. Present means the
* module answered us, i.e. the TX path is good; absent on a driver that
* asks is a strong hint the module never hears our configuration. */
if (zephcore_gnss_version_get != NULL) {
char ver[40];
if (zephcore_gnss_version_get(ver, sizeof(ver)) > 0) {
n += (size_t)snprintf(buf + n, len - n, " mod=%s", ver);
} else {
n += (size_t)snprintf(buf + n, len - n, " mod=no-reply");
}
if (n >= len) {
return;
}
}
if (path == GPS_CFG_UART) {
/* Bytes actually clocked out to the module. Note these are sent
* blind — this proves transmission, not acceptance. Constellation
* tallies below are the acceptance evidence. */
n += (size_t)snprintf(buf + n, len - n, " sent=%u/%uB",
gps_cfg_diag.cmds, gps_cfg_diag.bytes);
} else if (path == GPS_CFG_API) {
n += (size_t)snprintf(buf + n, len - n, " sys_ret=%d rate_ret=%d",
gps_cfg_diag.api_ret, gps_cfg_diag.rate_ret);
}
if (n >= len) {
return;
}
#ifdef CONFIG_ZEPHCORE_GPS_SAT_DIAG
k_mutex_lock(&gps_mutex, K_FOREVER);
/* Age out constellations that have stopped reporting, so a stale count
* is never presented as current. */
int64_t now_ms = k_uptime_get();
uint8_t shown[5];
for (int i = 0; i < 5; i++) {
shown[i] = ((now_ms - sat_seen_ms[i]) > SAT_TALLY_STALE_MS) ? 0 : sat_count[i];
}
k_mutex_unlock(&gps_mutex);
snprintf(buf + n, len - n, " sys=G%u/R%u/E%u/B%u/?%u",
shown[0], shown[1], shown[2], shown[3], shown[4]);
#else
snprintf(buf + n, len - n, " (build without GPS_SAT_DIAG: no per-constellation proof)");
#endif
#else
ARG_UNUSED(gps_diag_on);
snprintf(buf, len, "no GNSS on this board");
#endif
}
void gps_get_state_info(struct gps_state_info *info)
{
memset(info, 0, sizeof(*info));
@@ -1803,6 +2331,19 @@ void gps_get_state_info(struct gps_state_info *info)
} else if (gps_current_state == GPS_STATE_ACQUIRING) {
info->next_search_s = 0; /* Searching right now */
}
#ifdef CONFIG_ZEPHCORE_GPS_SAT_DIAG
/* Same staleness rule as the diag report — never present a count for a
* constellation that has stopped reporting. */
k_mutex_lock(&gps_mutex, K_FOREVER);
int64_t sat_now = k_uptime_get();
uint8_t *dst[5] = { &info->sats_gps, &info->sats_glonass, &info->sats_galileo,
&info->sats_beidou, &info->sats_other };
for (int i = 0; i < 5; i++) {
*dst[i] = ((sat_now - sat_seen_ms[i]) > SAT_TALLY_STALE_MS) ? 0 : sat_count[i];
}
k_mutex_unlock(&gps_mutex);
#endif
#endif
}
+25
View File
@@ -31,8 +31,33 @@ struct gps_state_info {
uint16_t satellites; /* Current/last satellite count */
uint32_t last_fix_age_s; /* Seconds since last validated fix (UINT32_MAX = never) */
uint32_t next_search_s; /* Seconds until next search (0 = searching now or off) */
/* Tracked satellites per constellation, from GSV talker IDs. Populated
* only when CONFIG_ZEPHCORE_GPS_SAT_DIAG is enabled; all zero otherwise.
* Proves whether multi-constellation configuration actually took a
* module left in its GPS-only default reports sats_gps only. */
uint8_t sats_gps;
uint8_t sats_glonass;
uint8_t sats_galileo;
uint8_t sats_beidou;
uint8_t sats_other;
};
/* ===== GPS configuration diagnostics (runtime toggle, RAM only) =====
*
* Module configuration (PMTK/UBX or the GNSS API) is sent blind at boot and
* runs exactly once. With diag on, the next GPS enable re-runs it and records
* what actually happened, so an operator can power-cycle the GPS ("gps off"
* then "gps on") and read the outcome back over the CLI on a release build
* no debug logging, no reflash.
*
* Not persisted: a diagnostic, not a setting. Clears on reboot. */
void gps_set_diag(bool on);
bool gps_get_diag(void);
/* Render the last configuration attempt as a single CLI line. Always
* succeeds; reports "never run" if configuration has not happened yet. */
void gps_get_diag_report(char *buf, size_t len);
/* GPS enable callback - called when GPS is enabled/disabled (for power management) */
typedef void (*gps_enable_callback_t)(bool enabled);
void gps_set_enable_callback(gps_enable_callback_t cb);
+3
View File
@@ -1102,6 +1102,9 @@ void RepeaterMesh::formatGpsStatsReply(char* reply) {
"on state=%s sats=%u no fix",
state, gsi.satellites);
}
/* Per-constellation tally is deliberately NOT appended here — "get gps"
* has to fit a LoRa reply. It lives in "get gps diag" instead. */
}
void RepeaterMesh::savePrefs() {
+8
View File
@@ -22,3 +22,11 @@ CONFIG_ENTROPY_GENERATOR=y
# Observer-style WiFi+MQTT reporting from repeater role.
# Runtime-configured and reboot-applied.
CONFIG_ZEPHCORE_REPEATER_UPLINK=n
# ========== GNSS dynamic model ==========
# Repeaters and room servers don't move. Tell CASIC-family GNSS modules so:
# the dynamic model is stored IN THE MODULE and survives reflashing the host,
# so a board that previously ran other GNSS firmware can arrive stuck in an
# automotive or airborne model that quietly degrades fixes on a fixed site.
# 1 = stationary. Only applies to boards using the air530z-family driver.
CONFIG_GNSS_LUATOS_AIR530Z_NAV_MODE=1
@@ -69,8 +69,16 @@
/* ---- Power control ---- */
/* 3V3_S rail must be HIGH for GPS and IO-slot peripherals.
* WB_IO2 (P1.02) doubles as GPS_RESET on the RAK1910 slot,
* so it must remain HIGH when GPS is active. */
*
* Never duty-cycle this rail on THIS board: WB_IO2 (P1.02) also gates the
* 5V boost feeding the SKY66122 PA, so dropping it to save GPS power kills
* the 1W transmit path. That is why it is `regulator-boot-on` and is NOT
* wired up as `chosen zephcore,gps-power`.
*
* (This pin also lands on RESET_N of a RAK1910/RAK12500 in Slot A, which is
* why it is sometimes called "GPS reset". It is not one u-blox specify
* RESET_N as a reset only, not an enable, since the SiP draws significant
* current while held in reset. See MeshCore PR #3051.) */
v3v3s_enable: v3v3s-enable {
compatible = "regulator-fixed";
regulator-name = "v3v3s-enable";
+1 -1
View File
@@ -6,7 +6,7 @@
#
# Hardware:
# - SX1262 LoRa on SPI0 (built into RAK4631 module)
# - RAK1910 GPS on UART0 (Slot A) - u-blox MAX-7Q at 9600 baud
# - RAK1910 GPS on UART1 (Slot A, P0.15/P0.16 = Serial1) - u-blox MAX-7Q at 9600 baud
# - Battery ADC on AIN3 (P0.05)
# - I2C0 for WisBlock sensors (auto-detected):
# - RAK1901 (SHTC3) at 0x70
+37 -4
View File
@@ -9,7 +9,7 @@
* Supports: P25Q16H, MX25R1635F, W25Q16, GD25Q16, IS25LP080, ZD25WQ16
* If no QSPI flash present, falls back to internal flash only.
*
* GPS: RAK1910 (u-blox MAX-7Q) on UART0 at 9600 baud (Slot A)
* GPS: RAK1910 (u-blox MAX-7Q) on UART1 at 9600 baud (Slot A)
* Sensors: Auto-detected on I2C0 - supports:
* - RAK1901 (SHTC3) at 0x70
* - RAK1902 (LPS22HB) at 0x5C
@@ -119,6 +119,26 @@
output-high;
line-name = "sx1262-power-en";
};
/* 3V3_S rail enable — WB_IO2 / P1.02, HIGH = on. This is the switched
* 3.3V bus on every WisBlock base board: it feeds the GPS slot, the
* sensor slot and the OLED. Without this the pin sits at its reset
* default (input/float) and the GPS module is simply never powered.
*
* Hogged rather than exposed as `chosen zephcore,gps-power`: the rail is
* not GPS-private, so duty-cycling it for the GPS would also drop the
* I2C sensors and the display.
*
* Note this pin is often described as "GPS reset" (it lands on the
* RAK1910/RAK12500 RESET_N in Slot A). Do not drive it as a reset —
* u-blox specify RESET_N as a reset only, not an enable (the SiP draws
* significant current while held in reset). See MeshCore PR #3051. */
v3v3s_en {
gpio-hog;
gpios = <2 GPIO_ACTIVE_HIGH>;
output-high;
line-name = "3v3s-enable";
};
};
/* LEDs are active-high on this board (stock RAK4631 DTS used active-low). */
@@ -131,16 +151,29 @@
};
/*
* GPS on UART0 - RAK1910 (u-blox MAX-7Q) at 9600 baud
* Connect to Slot A (UART pins P0.19 RX, P0.20 TX)
* GPS on UART1 - RAK1910 (u-blox MAX-7Q) at 9600 baud, Slot A.
*
* The WisBlock GPS slot is wired to the core module's Serial1 = P0.15 (MCU RX
* <- GPS TX) / P0.16 (MCU TX -> GPS RX), which is Zephyr's uart1 on this board
* (uart0 is P0.19/P0.20 = Serial2, which the GPS slot does not reach). Matches
* MeshCore variants/rak4631 (PIN_SERIAL1_RX 15 / _TX 16) and ZephCore's sibling
* RAK-family boards gat562_30s and rak3401_1watt. uart1 is also nrf-uarte
* upstream, whereas uart0 is the legacy nrf-uart.
*/
&uart0 {
&uart1 {
current-speed = <9600>;
gnss: gnss-nmea-generic {
compatible = "gnss-nmea-generic";
};
};
/* uart0 (P0.19/P0.20) is unused — console is USB CDC ACM and the GPS is on
* uart1. Upstream leaves it enabled at 115200; turn it off so it does not
* hold pins or a UARTE instance. */
&uart0 {
status = "disabled";
};
/* I2C0: optional SSD1306 + sensors */
&i2c0 {
ssd1306: ssd1306@3c {
@@ -9,8 +9,8 @@
* - SX1262 LoRa radio on SPI1 (DIO2 RF switch, DIO3 TCXO 3.3V, 22dBm)
* NSS=P1.12, DIO1=P1.06, RESET=P1.10, BUSY=P1.11
* - GPS L76K on UART0 (9600 baud, CASIC/Air530Z luatos,air530z driver)
* Control: GPS_EN=P0.06 (active-HIGH), GPS_RESET=P0.29 (active-LOW),
* GPS_STANDBY=P0.30, GPS_PPS=P0.31
* Control: GPS_EN=P0.06 (active-HIGH), GPS_RESET=P0.29 (REINIT must
* float, see below), GPS_STANDBY=P0.30, GPS_PPS=P0.31
* - Battery ADC on AIN4 (P0.28), 1.75:1 voltage divider
* ADC_CTRL enable pin: P0.11 (gpio-hog, always HIGH)
* - LEDs: RED=P0.12, BLUE=P0.07 (both active-HIGH, LED_STATE_ON=HIGH)
@@ -138,9 +138,8 @@
};
};
/* GPS_RESET (P0.29) is driven statically HIGH (de-asserted) by a gpio-hog
* in &gpio0 below see the note there. Arduino MeshCore does the same
* (variant.cpp: digitalWrite(PIN_GPS_RESET, HIGH), never pulsed). */
/* GPS_RESET (P0.29) is the module's REINIT line and is intentionally left
* floating (unconfigured input) see the note in &gpio0 below. */
aliases {
led0 = &led_red;
@@ -154,8 +153,8 @@
* L76K (CASIC/Air530Z) is driven by the luatos,air530z driver via
* on-off-gpios, and exposing those aliases would push M6 down the
* T1000-E GPS sequence in ZephyrGPSManager (HAS_GPS_SLEEP/RESET).
* GPS_RESET (P0.29) and GPS_STANDBY (P0.30) are hogged HIGH in
* &gpio0 instead static drive, matching Arduino MeshCore. */
* GPS_STANDBY (P0.30) is hogged HIGH in &gpio0; GPS_RESET (P0.29)
* is left floating see the note there. */
gps-enable = &gps_enable_pin;
};
};
@@ -199,13 +198,20 @@
* (see the / node) not hogged HIGH so the VBAT divider only draws
* current during a battery sample, matching Arduino MeshCore. */
/* GPS control lines held static, mirroring Arduino MeshCore variant.cpp
* (GPS_EN is driven separately by the luatos,air530z on-off-gpios):
* GPS_STANDBY (P0.30) HIGH L76K runs, never parked in standby.
* GPS_RESET (P0.29) HIGH reset de-asserted (active-LOW line).
/* GPS control lines (GPS_EN is driven separately by the luatos,air530z
* on-off-gpios):
* GPS_STANDBY (P0.30) hogged HIGH L76K runs, never parked in standby.
* GPS_RESET (P0.29) NOT driven this is the module's REINIT line and
* must float. Driving it (in particular HIGH, which this board used to
* do via a gpio-hog on the mistaken premise that HIGH was a de-asserted
* active-LOW reset) holds the L76K silent: it emits no NMEA at all and
* GPS is never detected. Upstream MeshCore bench-tested a sealed M6
* (passive NMEA capture): pin driven HIGH = 0 bytes, floating = full
* stream so they set GPS_RESET = -1 (5c534c43). Meshtastic likewise
* leaves the pin an input.
* M6 has no physical GPS switch; runtime on/off is via GPS_EN
* (gps-enable alias) under software/CLI control. Hogs are used so the
* pins are actively driven at boot without needing a consumer the
* (gps-enable alias) under software/CLI control. A hog is used for STANDBY
* so the pin is actively driven at boot without needing a consumer the
* gpio-leds nodes are not bound by any driver in this build. */
gps-standby-hog {
gpio-hog;
@@ -213,13 +219,6 @@
output-high;
line-name = "GPS Standby";
};
gps-reset-hog {
gpio-hog;
gpios = <29 GPIO_ACTIVE_HIGH>;
output-high;
line-name = "GPS Reset";
};
};
&gpio1 {
@@ -233,7 +232,8 @@
* via on-off-gpios (P0.06 = GPS_EN, shared with the gps-enable alias used for
* runtime power control in ZephyrGPSManager). No zephyr,deferred-init: the
* driver powers the module on-off-gpios HIGH at init before opening the pipe.
* GPS_RESET (P0.29) and GPS_STANDBY (P0.30) are hogged HIGH in &gpio0. */
* GPS_STANDBY (P0.30) is hogged HIGH in &gpio0; GPS_RESET (P0.29) is left
* floating (driving it silences the module see the note there). */
&uart0 {
compatible = "nordic,nrf-uarte";
status = "okay";
@@ -5,10 +5,16 @@
* Battery: AIN7 = P0.31, voltage divider 2:1.
*/
/* GPS power control - P1.09
* The luatos,air530z driver also manages this pin via on-off-gpios,
* but we need our own alias so ZephyrSensorManager can toggle power
* independently (driver PM suspend/resume hangs on modem_pipe ops). */
/* GPS standby control P1.09 = the L76K's WAKEUP pin (schematic net
* GNSS_WAKEUP). This is NOT a supply switch: per the L76K hardware design the
* pin only enters/exits Standby, where the RF is off but the core and I/O
* domain stay powered. VCC is never removed, so ephemeris/almanac/RTC are
* retained and each wake is a warm start. Active low with an internal
* pull-up, which is why the module runs even with this pin unconfigured.
*
* The luatos,air530z driver also manages this pin via on-off-gpios, but we
* need our own alias so ZephyrSensorManager can toggle it independently
* (driver PM suspend/resume hangs on modem_pipe ops). */
/ {
gps_en: gps-enable {
compatible = "gpio-leds";
@@ -188,9 +188,11 @@
/* NOTE: buzzer-off gpio-hog REMOVED - buzzer pin is now managed by PWM
* driver. When PWM is not driving, the pin is low (no current draw). */
/* NOTE: GPS enable hog REMOVED - Feb 6 working version had no GPIO hog
* and GPS worked fine. The L76KB module may have internal pull-up or
* the Arduino bootloader leaves it enabled. */
/* NOTE: GPS enable hog REMOVED GPS works fine without it, and the
* reason is now confirmed rather than guessed: P1.09 is the L76K's
* WAKEUP pin, which the L76K hardware design specifies as active low
* "with pull-up internally". Left unconfigured it floats high and the
* module stays in Continuous mode. */
};
/* PWM0 for buzzer */
@@ -207,7 +209,8 @@
/* Quectel L76KB GNSS on UART0 - 9600 baud (PCAS/CASIC protocol)
* Uses Zephyr luatos,air530z driver (same PCAS command set for
* constellation config via PCAS04, fix rate via PCAS02, etc.)
* on-off-gpios = GPS enable pin (P1.09), managed by driver. */
* on-off-gpios = P1.09, the module's WAKEUP/standby pin (see &gpio1 note and
* board.overlay) driver drives it high for Continuous, low for Standby. */
&uart0 {
compatible = "nordic,nrf-uarte";
status = "okay";
+24
View File
@@ -573,6 +573,10 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
}
} else if (memcmp(config, "rxduty", 6) == 0) {
snprintf(reply, CLI_REPLY_SIZE, "> %d", (int)_prefs->rx_duty_cycle);
} else if (memcmp(config, "gps diag", 8) == 0) {
// What the last module-configuration attempt actually did.
reply[0] = '>'; reply[1] = ' ';
gps_get_diag_report(reply + 2, CLI_REPLY_SIZE - 2);
} else if (memcmp(config, "gps duty", 8) == 0) {
uint32_t s = gps_get_poll_interval_sec(); // now-effective value
if (s == 0) strcpy(reply, "> always on (0)");
@@ -1066,6 +1070,26 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
} else {
strcpy(reply, "Error: must be 0, 1, on, or off");
}
} else if (memcmp(config, "gps diag", 8) == 0) {
// set gps diag <0|1|on|off> — arm module-configuration reporting.
// Not persisted: clears on reboot, by design.
const char* arg = config + 8;
while (*arg == ' ') arg++;
int val = -1;
if (memcmp(arg, "on", 2) == 0) val = 1;
else if (memcmp(arg, "off", 3) == 0) val = 0;
else if (arg[0] == '0' || arg[0] == '1') val = atoi(arg);
if (val == 0 || val == 1) {
gps_set_diag(val == 1);
if (val == 1) {
strcpy(reply, "OK - gps diag on; run 'gps off' then 'gps on', "
"then 'get gps diag'");
} else {
strcpy(reply, "OK - gps diag off");
}
} else {
strcpy(reply, "usage: set gps diag <0|1|on|off>");
}
} else if (memcmp(config, "gps duty", 8) == 0) {
// set gps duty <seconds> | default (0 = always on)
const char* arg = config + 8;
@@ -1,119 +0,0 @@
diff --git a/drivers/gnss/Kconfig.luatos_air530z b/drivers/gnss/Kconfig.luatos_air530z
index c5d09261da5..ffc218b22e2 100644
--- a/drivers/gnss/Kconfig.luatos_air530z
+++ b/drivers/gnss/Kconfig.luatos_air530z
@@ -28,4 +28,16 @@ config GNSS_LUATOS_AIR530Z_SATELLITES_COUNT
the device is actually tracking, just how many of those can
be reported in the satellites callback.
+config GNSS_LUATOS_AIR530Z_EASY
+ bool "Enable EASY (Embedded Assist System) for faster TTFF"
+ default y
+ help
+ Enable MediaTek EASY (Embedded Assist System) mode via PMTK869
+ command. EASY caches predicted ephemeris in the GNSS module's
+ internal flash, reducing Time-To-First-Fix from 15-45s (cold)
+ to 1-3s (warm). Sent on every driver init (boot and PM resume).
+ The setting persists in GNSS flash, so resending is a no-op.
+ Compatible with L76K/L76KB and other MediaTek-based GNSS chips
+ that accept PMTK commands alongside PCAS.
+
endif
diff --git a/drivers/gnss/gnss_luatos_air530z.c b/drivers/gnss/gnss_luatos_air530z.c
index 74708edbd62..7de70ad93c8 100644
--- a/drivers/gnss/gnss_luatos_air530z.c
+++ b/drivers/gnss/gnss_luatos_air530z.c
@@ -9,7 +9,6 @@
#include <zephyr/modem/chat.h>
#include <zephyr/modem/backend/uart.h>
#include <zephyr/kernel.h>
-#include <zephyr/pm/device.h>
#include <zephyr/drivers/gpio.h>
#include <string.h>
@@ -36,6 +35,13 @@ MODEM_CHAT_SCRIPT_CMDS_DEFINE(init_script_cmds,
/* receive only GGA and RMC NMEA messages */
MODEM_CHAT_SCRIPT_CMD_RESP_NONE("$PCAS03,1,0,0,0,1,0,0,0,0,0,0,0,0*1E", 10),
#endif
+#if IS_ENABLED(CONFIG_GNSS_LUATOS_AIR530Z_EASY)
+ /* Enable EASY (Embedded Assist System) — caches predicted ephemeris
+ * in GNSS internal flash for 1-3s warm start instead of 15-45s cold.
+ * PMTK869,1,1 = Set EASY, Enable. Persists across power cycles.
+ * L76K/L76KB accept PMTK alongside PCAS (both MediaTek MT33xx). */
+ MODEM_CHAT_SCRIPT_CMD_RESP_NONE("$PMTK869,1,1*36", 10),
+#endif
);
MODEM_CHAT_SCRIPT_NO_ABORT_DEFINE(init_script, init_script_cmds, NULL, 5);
@@ -217,54 +223,11 @@ static int gnss_luatos_air530z_init(const struct device *dev)
return 0;
}
-static int luatos_air530z_pm_resume(const struct device *dev)
-{
- struct gnss_luatos_air530z_data *data = dev->data;
- int ret;
-
- ret = modem_pipe_open(data->uart_pipe, K_SECONDS(10));
- if (ret < 0) {
- return ret;
- }
-
- ret = modem_chat_attach(&data->chat, data->uart_pipe);
- if (ret < 0) {
- modem_pipe_close(data->uart_pipe, K_SECONDS(10));
- return ret;
- }
-
- ret = modem_chat_run_script(&data->chat, &init_script);
- if (ret < 0) {
- modem_pipe_close(data->uart_pipe, K_SECONDS(10));
- return ret;
- }
-
- return 0;
-}
-
-static int luatos_air530z_pm_action(const struct device *dev, enum pm_device_action action)
-{
- struct gnss_luatos_air530z_data *data = dev->data;
- const struct gnss_luatos_air530z_config *config = dev->config;
- int ret = -ENOTSUP;
-
- switch (action) {
- case PM_DEVICE_ACTION_SUSPEND:
- gpio_pin_set_dt(&config->on_off_gpio, 0);
- ret = modem_pipe_close(data->uart_pipe, K_SECONDS(10));
- break;
-
- case PM_DEVICE_ACTION_RESUME:
- gpio_pin_set_dt(&config->on_off_gpio, 1);
- ret = luatos_air530z_pm_resume(dev);
- break;
-
- default:
- break;
- }
-
- return ret;
-}
+/* PM hooks intentionally removed — the L76K ignores $PMTK161,0 (AT6558-
+ * based, not genuine MediaTek), so PM-driven software sleep can never
+ * work; standby drives the FORCE_ON GPIO and the app suspends the UART
+ * instead (ZephyrGPSManager, main thread only). PM-less also keeps
+ * modem_chat scripts out of PM callbacks (deadlock risk off sysworkq). */
static int luatos_air530z_set_fix_rate(const struct device *dev, uint32_t fix_interval_ms)
{
@@ -356,10 +319,8 @@ static DEVICE_API(gnss, gnss_api) = {
.dynamic_separators_buf = {',', '*'}, \
}; \
\
- PM_DEVICE_DT_INST_DEFINE(inst, luatos_air530z_pm_action); \
- \
DEVICE_DT_INST_DEFINE(inst, gnss_luatos_air530z_init, \
- PM_DEVICE_DT_INST_GET(inst), \
+ NULL, \
&gnss_luatos_air530z_data_##inst, \
&gnss_luatos_air530z_cfg_##inst, \
POST_KERNEL, CONFIG_GNSS_INIT_PRIORITY, &gnss_api);
@@ -0,0 +1,371 @@
diff --git a/drivers/gnss/Kconfig.luatos_air530z b/drivers/gnss/Kconfig.luatos_air530z
index c5d09261da5..94ff2848b88 100644
--- a/drivers/gnss/Kconfig.luatos_air530z
+++ b/drivers/gnss/Kconfig.luatos_air530z
@@ -28,4 +28,39 @@ config GNSS_LUATOS_AIR530Z_SATELLITES_COUNT
the device is actually tracking, just how many of those can
be reported in the satellites callback.
+config GNSS_LUATOS_AIR530Z_NAV_MODE
+ int "Navigation dynamic model ($PCAS11)"
+ range -1 7
+ default 3
+ help
+ Dynamic model sent to the module at init: 0 = portable,
+ 1 = stationary, 2 = pedestrian, 3 = automotive, 4 = sea,
+ 5-7 = airborne. Set -1 to send nothing and leave the module on
+ whatever it already has.
+
+ Set this even though it looks like a tuning detail. The dynamic
+ model is stored IN THE MODULE and survives reflashing the host, so
+ a board that previously ran other GNSS firmware can arrive stuck in
+ an airborne or automotive model that quietly degrades fixes for a
+ stationary node. Repeaters and room servers should use 1.
+
+ $PCAS11 is not in the L76K protocol specification (which documents
+ only PCAS01/02/03/04/10); it is an undocumented CASIC command these
+ parts accept in practice. Mode 1 is confirmed against a published
+ CASIC command reference.
+
+config GNSS_LUATOS_AIR530Z_VERSION_QUERY
+ bool "Query the module version at init ($PCAS06)"
+ default y
+ help
+ Send $PCAS06,0 at init and capture the module's identification
+ string from the $GPTXT reply it sends back in-band.
+
+ Besides identifying the part, this is the only positive proof
+ available on this transport that the MCU's TX line actually reaches
+ the module — every other command in the init script is written
+ blind, so a module that hears nothing is indistinguishable from one
+ that hears everything and ignores it. The captured string is
+ reported by the application's GPS diagnostics.
+
endif
diff --git a/drivers/gnss/gnss_luatos_air530z.c b/drivers/gnss/gnss_luatos_air530z.c
index 74708edbd62..7fc5e4768f1 100644
--- a/drivers/gnss/gnss_luatos_air530z.c
+++ b/drivers/gnss/gnss_luatos_air530z.c
@@ -9,7 +9,6 @@
#include <zephyr/modem/chat.h>
#include <zephyr/modem/backend/uart.h>
#include <zephyr/kernel.h>
-#include <zephyr/pm/device.h>
#include <zephyr/drivers/gpio.h>
#include <string.h>
@@ -28,16 +27,75 @@ LOG_MODULE_REGISTER(luatos_air530z, CONFIG_GNSS_LOG_LEVEL);
#define CHAT_RECV_BUF_SZ 256
#define CHAT_ARGV_SZ 32
+/* Inter-command delay. These parts are CASIC silicon on a 9600-baud link and
+ * some commands (constellation selection especially) restart the navigation
+ * engine, so the module needs real settling time between sentences. 10 ms is
+ * not enough — a command issued into a restarting engine is simply lost.
+ */
+#define AIR530Z_CMD_DELAY_MS 250
+
+/* Navigation dynamic model — $PCAS11,<mode>. Undocumented in the L76K
+ * protocol specification (which lists only PCAS01/02/03/04/10) but accepted
+ * by these parts in practice. Mode 1 = stationary is confirmed against a
+ * published CASIC command reference; the remaining values follow the usual
+ * dynamic-model ladder.
+ *
+ * Worth setting explicitly even though it looks like a tuning detail: the
+ * dynamic model PERSISTS IN THE MODULE across firmware flashes, so a board
+ * that previously ran other GNSS firmware can arrive stuck in an airborne or
+ * automotive model that quietly degrades fixes for a stationary node.
+ */
+#if CONFIG_GNSS_LUATOS_AIR530Z_NAV_MODE == 0
+#define AIR530Z_NAV_CMD "$PCAS11,0*1D"
+#elif CONFIG_GNSS_LUATOS_AIR530Z_NAV_MODE == 1
+#define AIR530Z_NAV_CMD "$PCAS11,1*1C"
+#elif CONFIG_GNSS_LUATOS_AIR530Z_NAV_MODE == 2
+#define AIR530Z_NAV_CMD "$PCAS11,2*1F"
+#elif CONFIG_GNSS_LUATOS_AIR530Z_NAV_MODE == 3
+#define AIR530Z_NAV_CMD "$PCAS11,3*1E"
+#elif CONFIG_GNSS_LUATOS_AIR530Z_NAV_MODE == 4
+#define AIR530Z_NAV_CMD "$PCAS11,4*19"
+#elif CONFIG_GNSS_LUATOS_AIR530Z_NAV_MODE == 5
+#define AIR530Z_NAV_CMD "$PCAS11,5*18"
+#elif CONFIG_GNSS_LUATOS_AIR530Z_NAV_MODE == 6
+#define AIR530Z_NAV_CMD "$PCAS11,6*1B"
+#elif CONFIG_GNSS_LUATOS_AIR530Z_NAV_MODE == 7
+#define AIR530Z_NAV_CMD "$PCAS11,7*1A"
+#endif
+
MODEM_CHAT_SCRIPT_CMDS_DEFINE(init_script_cmds,
#if CONFIG_GNSS_SATELLITES
/* receive only GGA, RMC and GSV NMEA messages */
- MODEM_CHAT_SCRIPT_CMD_RESP_NONE("$PCAS03,1,0,0,1,1,0,0,0,0,0,0,0,0*1F", 10),
+ MODEM_CHAT_SCRIPT_CMD_RESP_NONE("$PCAS03,1,0,0,1,1,0,0,0,0,0,0,0,0*1F",
+ AIR530Z_CMD_DELAY_MS),
#else
/* receive only GGA and RMC NMEA messages */
- MODEM_CHAT_SCRIPT_CMD_RESP_NONE("$PCAS03,1,0,0,0,1,0,0,0,0,0,0,0,0*1E", 10),
+ MODEM_CHAT_SCRIPT_CMD_RESP_NONE("$PCAS03,1,0,0,0,1,0,0,0,0,0,0,0,0*1E",
+ AIR530Z_CMD_DELAY_MS),
+#endif
+#ifdef AIR530Z_NAV_CMD
+ MODEM_CHAT_SCRIPT_CMD_RESP_NONE(AIR530Z_NAV_CMD, AIR530Z_CMD_DELAY_MS),
+#endif
+#if IS_ENABLED(CONFIG_GNSS_LUATOS_AIR530Z_VERSION_QUERY)
+ /* Ask the module to identify itself. The reply arrives in-band as an
+ * ordinary $GPTXT NMEA sentence (see the unsolicited match below), so
+ * it costs nothing to read and needs no raw access to the UART.
+ *
+ * This is the only positive proof available on this transport that the
+ * MCU's TX line actually reaches the module: every other command here
+ * is written blind. If configuration appears to have no effect, a
+ * missing version string says the module never heard any of it.
+ */
+ MODEM_CHAT_SCRIPT_CMD_RESP_NONE("$PCAS06,0*1B", AIR530Z_CMD_DELAY_MS),
#endif
);
+/* NOTE: configuration is deliberately NOT persisted to the module ($PCAS00).
+ * The init script re-sends it on every boot, and a saved configuration is
+ * sticky — including a wrong one, which then survives reflashing and has to
+ * be hunted down on hardware.
+ */
+
MODEM_CHAT_SCRIPT_NO_ABORT_DEFINE(init_script, init_script_cmds, NULL, 5);
struct gnss_luatos_air530z_config {
@@ -73,12 +131,57 @@ struct gnss_luatos_air530z_data {
struct k_sem lock;
};
+#if IS_ENABLED(CONFIG_GNSS_LUATOS_AIR530Z_VERSION_QUERY)
+/* Module identification string, captured from the $PCAS06 reply.
+ *
+ * $GPTXT,01,01,02,SW=<version>,... — the interesting field is the one
+ * starting "SW=". Stored verbatim (truncated) rather than parsed: this is a
+ * diagnostic, and different parts word the reply differently.
+ */
+static char air530z_version[40];
+
+static void air530z_gptxt_callback(struct modem_chat *chat, char **argv, uint16_t argc,
+ void *user_data)
+{
+ ARG_UNUSED(chat);
+ ARG_UNUSED(user_data);
+
+ /* Take the first field that looks like a software-version token. */
+ for (uint16_t i = 1; i < argc; i++) {
+ if (strncmp(argv[i], "SW=", 3) == 0) {
+ strncpy(air530z_version, argv[i] + 3, sizeof(air530z_version) - 1);
+ air530z_version[sizeof(air530z_version) - 1] = '\0';
+ LOG_INF("GNSS module version: %s", air530z_version);
+ return;
+ }
+ }
+}
+
+/* Exported for application-level diagnostics. Returns the number of
+ * characters written, or 0 if the module has not identified itself — which
+ * is itself the useful signal (no reply => the module is not receiving).
+ */
+__weak int zephcore_gnss_version_get(char *buf, size_t len)
+{
+ if (buf == NULL || len == 0 || air530z_version[0] == '\0') {
+ return 0;
+ }
+
+ strncpy(buf, air530z_version, len - 1);
+ buf[len - 1] = '\0';
+ return (int)strlen(buf);
+}
+#endif /* CONFIG_GNSS_LUATOS_AIR530Z_VERSION_QUERY */
+
MODEM_CHAT_MATCHES_DEFINE(unsol_matches,
MODEM_CHAT_MATCH_WILDCARD("$??GGA,", ",*", gnss_nmea0183_match_gga_callback),
MODEM_CHAT_MATCH_WILDCARD("$??RMC,", ",*", gnss_nmea0183_match_rmc_callback),
#if CONFIG_GNSS_SATELLITES
MODEM_CHAT_MATCH_WILDCARD("$??GSV,", ",*", gnss_nmea0183_match_gsv_callback),
#endif
+#if IS_ENABLED(CONFIG_GNSS_LUATOS_AIR530Z_VERSION_QUERY)
+ MODEM_CHAT_MATCH_WILDCARD("$??TXT,", ",*", air530z_gptxt_callback),
+#endif
);
static void luatos_air530z_lock(const struct device *dev)
@@ -217,54 +320,11 @@ static int gnss_luatos_air530z_init(const struct device *dev)
return 0;
}
-static int luatos_air530z_pm_resume(const struct device *dev)
-{
- struct gnss_luatos_air530z_data *data = dev->data;
- int ret;
-
- ret = modem_pipe_open(data->uart_pipe, K_SECONDS(10));
- if (ret < 0) {
- return ret;
- }
-
- ret = modem_chat_attach(&data->chat, data->uart_pipe);
- if (ret < 0) {
- modem_pipe_close(data->uart_pipe, K_SECONDS(10));
- return ret;
- }
-
- ret = modem_chat_run_script(&data->chat, &init_script);
- if (ret < 0) {
- modem_pipe_close(data->uart_pipe, K_SECONDS(10));
- return ret;
- }
-
- return 0;
-}
-
-static int luatos_air530z_pm_action(const struct device *dev, enum pm_device_action action)
-{
- struct gnss_luatos_air530z_data *data = dev->data;
- const struct gnss_luatos_air530z_config *config = dev->config;
- int ret = -ENOTSUP;
-
- switch (action) {
- case PM_DEVICE_ACTION_SUSPEND:
- gpio_pin_set_dt(&config->on_off_gpio, 0);
- ret = modem_pipe_close(data->uart_pipe, K_SECONDS(10));
- break;
-
- case PM_DEVICE_ACTION_RESUME:
- gpio_pin_set_dt(&config->on_off_gpio, 1);
- ret = luatos_air530z_pm_resume(dev);
- break;
-
- default:
- break;
- }
-
- return ret;
-}
+/* PM hooks intentionally removed — the L76K ignores $PMTK161,0 (AT6558-
+ * based, not genuine MediaTek), so PM-driven software sleep can never
+ * work; standby drives the FORCE_ON GPIO and the app suspends the UART
+ * instead (ZephyrGPSManager, main thread only). PM-less also keeps
+ * modem_chat scripts out of PM callbacks (deadlock risk off sysworkq). */
static int luatos_air530z_set_fix_rate(const struct device *dev, uint32_t fix_interval_ms)
{
@@ -356,10 +416,8 @@ static DEVICE_API(gnss, gnss_api) = {
.dynamic_separators_buf = {',', '*'}, \
}; \
\
- PM_DEVICE_DT_INST_DEFINE(inst, luatos_air530z_pm_action); \
- \
DEVICE_DT_INST_DEFINE(inst, gnss_luatos_air530z_init, \
- PM_DEVICE_DT_INST_GET(inst), \
+ NULL, \
&gnss_luatos_air530z_data_##inst, \
&gnss_luatos_air530z_cfg_##inst, \
POST_KERNEL, CONFIG_GNSS_INIT_PRIORITY, &gnss_api);
diff --git a/drivers/gnss/gnss_nmea0183.c b/drivers/gnss/gnss_nmea0183.c
index d4e7b502e39..c8e8f2da2d2 100644
--- a/drivers/gnss/gnss_nmea0183.c
+++ b/drivers/gnss/gnss_nmea0183.c
@@ -52,6 +52,14 @@ static int gnss_system_from_gsv_header_args(const struct gsv_header_args *args,
case 'B':
*sv_system = GNSS_SYSTEM_BEIDOU;
break;
+ case 'D':
+ /* "$BDGSV" — the pre-4.10 BeiDou talker, still emitted by CASIC
+ * receivers (Quectel L76K and relatives) and by u-blox left at
+ * their default NMEA version. Without this the entire group is
+ * rejected and BeiDou silently reads as zero satellites.
+ * 'D' at index 2 is unique to the BD talker. */
+ *sv_system = GNSS_SYSTEM_BEIDOU;
+ break;
case 'P':
*sv_system = GNSS_SYSTEM_GPS;
break;
diff --git a/drivers/gnss/gnss_nmea_generic.c b/drivers/gnss/gnss_nmea_generic.c
index bef7989828d..5e5a90716fb 100644
--- a/drivers/gnss/gnss_nmea_generic.c
+++ b/drivers/gnss/gnss_nmea_generic.c
@@ -50,12 +50,83 @@ struct gnss_nmea_generic_data {
uint8_t *chat_argv[CHAT_ARGV_SZ];
};
+/* Module identification, captured from a $GPTXT sentence.
+ *
+ * This driver is a passive listener — every configuration command the
+ * application sends is written blind, and nothing on this transport confirms
+ * the module received any of it. A CASIC part answers a $PCAS06 version query
+ * in-band with $GPTXT, and u-blox emit $GNTXT boot banners unprompted, so a
+ * captured string here is the one positive proof that the MCU's TX line
+ * reaches the module (or, for the u-blox banner, that RX is alive and the
+ * part is identified). Absence is the diagnostic signal.
+ */
+static char nmea_generic_version[40];
+
+static void nmea_generic_txt_callback(struct modem_chat *chat, char **argv, uint16_t argc,
+ void *user_data)
+{
+ ARG_UNUSED(chat);
+ ARG_UNUSED(user_data);
+
+ /* Prefer an explicit software-version token; otherwise keep the last
+ * text field, which is where u-blox put their boot banner. */
+ for (uint16_t i = 1; i < argc; i++) {
+ if (strncmp(argv[i], "SW=", 3) == 0) {
+ strncpy(nmea_generic_version, argv[i] + 3,
+ sizeof(nmea_generic_version) - 1);
+ nmea_generic_version[sizeof(nmea_generic_version) - 1] = '\0';
+ LOG_INF("GNSS module version: %s", nmea_generic_version);
+ return;
+ }
+ }
+
+ if (argc > 4 && nmea_generic_version[0] == '\0') {
+ strncpy(nmea_generic_version, argv[4], sizeof(nmea_generic_version) - 1);
+ nmea_generic_version[sizeof(nmea_generic_version) - 1] = '\0';
+ }
+}
+
+/* u-blox reply to a $PUBX poll. u-blox ignore the CASIC $PCAS06 query, so
+ * without this a u-blox module always reads as "no reply" and the TX-path
+ * proof is unavailable on exactly the boards that most need it. The content
+ * is time/status, not a version — what matters is that a reply arrived at
+ * all, which is what proves the module received our command. */
+static void nmea_generic_pubx_callback(struct modem_chat *chat, char **argv, uint16_t argc,
+ void *user_data)
+{
+ ARG_UNUSED(chat);
+ ARG_UNUSED(argv);
+ ARG_UNUSED(argc);
+ ARG_UNUSED(user_data);
+
+ if (nmea_generic_version[0] == '\0') {
+ strcpy(nmea_generic_version, "u-blox");
+ LOG_INF("GNSS module replied to $PUBX poll (u-blox family)");
+ }
+}
+
+/* Exported for application-level diagnostics. Returns characters written, or
+ * 0 if the module never identified itself. Weak so a board that also builds
+ * another GNSS driver exporting this symbol still links. */
+__weak int zephcore_gnss_version_get(char *buf, size_t len)
+{
+ if (buf == NULL || len == 0 || nmea_generic_version[0] == '\0') {
+ return 0;
+ }
+
+ strncpy(buf, nmea_generic_version, len - 1);
+ buf[len - 1] = '\0';
+ return (int)strlen(buf);
+}
+
MODEM_CHAT_MATCHES_DEFINE(unsol_matches,
MODEM_CHAT_MATCH_WILDCARD("$??GGA,", ",*", gnss_nmea0183_match_gga_callback),
MODEM_CHAT_MATCH_WILDCARD("$??RMC,", ",*", gnss_nmea0183_match_rmc_callback),
#if CONFIG_GNSS_SATELLITES
MODEM_CHAT_MATCH_WILDCARD("$??GSV,", ",*", gnss_nmea0183_match_gsv_callback),
#endif
+ MODEM_CHAT_MATCH_WILDCARD("$??TXT,", ",*", nmea_generic_txt_callback),
+ MODEM_CHAT_MATCH("$PUBX", ",*", nmea_generic_pubx_callback),
);
static int gnss_nmea_generic_resume(const struct device *dev)
+113
View File
@@ -0,0 +1,113 @@
# SPDX-License-Identifier: MIT
# ZephCore crypto benchmark is our crypto worth accelerating?
#
# Times every crypto primitive the mesh uses (SHA-256, AES-128-ECB,
# HMAC-SHA256, Ed25519, X25519), then divides the per-packet total by LoRa
# airtime. See HANDOVER_crypto_hw_accel.md this tool is the "measure first"
# step that gates every option in that document.
#
# It compiles src/Utils.cpp, src/Identity.cpp and src/Packet.cpp straight from
# the main tree, so what is measured is the shipping implementation, not a
# copy of it.
#
# Build:
# west build -b rak4631 zephcore/tools/crypto_bench --pristine
# west build -b xiao_esp32s3/esp32s3/procpu zephcore/tools/crypto_bench --pristine
#
# Iteration counts:
# west build ... zephcore/tools/crypto_bench -- -DCRYPTO_BENCH_ITERS=1000
# west build ... zephcore/tools/crypto_bench -- -DCRYPTO_BENCH_ITERS_SLOW=100
#
# Group-channel worst case (default 40 = MAX_CHANNELS):
# west build ... zephcore/tools/crypto_bench -- -DCRYPTO_BENCH_CHANNELS=8
#
# Flash: nRF52 drag build/zephyr/zephyr.uf2 onto the UF2 drive
# ESP32 west flash --esp-device COMx
#
# The tool never reads or writes stored identity, and never brings up the
# radio, flash or BLE. Reflash normal firmware afterwards.
cmake_minimum_required(VERSION 3.20.0)
# Reuse the main project's custom board definitions
list(APPEND BOARD_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/../..)
# Strip the board qualifier (e.g. "/esp32s3/procpu") to get the base name
string(REPLACE "/" ";" BOARD_PARTS ${BOARD})
list(GET BOARD_PARTS 0 BOARD_BASE)
# Tool-local board overlay ONLY deliberately not the node's board.overlay,
# which declares /fstab, radio, sensor and partition nodes this tool does not
# enable (CONFIG_FLASH=n, SPI=n, I2C=n) and would fail on an undefined
# lfs_partition label. Same arrangement as tools/rng_selftest.
set(_BENCH_OVERLAY
"${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOARD_BASE}/board.overlay")
if(EXISTS "${_BENCH_OVERLAY}")
set(EXTRA_DTC_OVERLAY_FILE "${_BENCH_OVERLAY}" CACHE STRING "" FORCE)
message(STATUS "crypto_bench overlay: ${_BENCH_OVERLAY}")
else()
message(STATUS "crypto_bench: using upstream board console (no overlay)")
endif()
# Per-board conf (UF2 output, code partition) nRF52 needs it, ESP32 must not
# have it. See boards/rak4631/board.conf.
set(_BENCH_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOARD_BASE}/board.conf")
if(EXISTS "${_BENCH_CONF}")
set(EXTRA_CONF_FILE "${_BENCH_CONF}" CACHE STRING "" FORCE)
message(STATUS "crypto_bench conf: ${_BENCH_CONF}")
endif()
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(zephcore_crypto_bench)
# The point of this tool is to time the SHIPPING code. These three files are
# compiled from the main tree, at the same optimisation level the node uses,
# so the numbers transfer.
#
# ZephyrRNG.cpp is deliberately NOT linked: nothing here needs entropy (all
# test vectors are fixed, on purpose see src/main.cpp), and pulling it in
# would drag CONFIG_ENTROPY_GENERATOR and hwinfo into a config that otherwise
# has no reason for them. The one thing it does that this tool needs
# psa_crypto_init() main.cpp calls itself.
target_sources(app PRIVATE
src/main.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../../src/Utils.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../../src/Identity.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../../src/Packet.cpp
)
# Ed25519 / X25519 backend the real one Identity.cpp uses.
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../../lib/monocypher monocypher)
target_link_libraries(app PRIVATE monocypher)
target_include_directories(app PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_CURRENT_SOURCE_DIR}/../../include
${CMAKE_CURRENT_SOURCE_DIR}/../../lib/monocypher
)
# Iteration counts. The headline figure is the MINIMUM over N, so N mainly
# buys confidence that no run escaped interference; 200 is plenty for the
# symmetric primitives.
if(NOT DEFINED CRYPTO_BENCH_ITERS)
set(CRYPTO_BENCH_ITERS 200)
endif()
# Ed25519 is ~1000x slower than SHA-256. 20 iterations keeps the total runtime
# in seconds; the variance on an operation that long is negligible anyway.
if(NOT DEFINED CRYPTO_BENCH_ITERS_SLOW)
set(CRYPTO_BENCH_ITERS_SLOW 20)
endif()
# Worst-case group-channel loop (Mesh.cpp:398 tries every configured channel).
# Default matches MAX_CHANNELS.
if(NOT DEFINED CRYPTO_BENCH_CHANNELS)
set(CRYPTO_BENCH_CHANNELS 40)
endif()
target_compile_definitions(app PRIVATE
CRYPTO_BENCH_ITERS=${CRYPTO_BENCH_ITERS}
CRYPTO_BENCH_ITERS_SLOW=${CRYPTO_BENCH_ITERS_SLOW}
CRYPTO_BENCH_CHANNELS=${CRYPTO_BENCH_CHANNELS}
)
message(STATUS "crypto_bench: N=${CRYPTO_BENCH_ITERS} slow=${CRYPTO_BENCH_ITERS_SLOW} channels=${CRYPTO_BENCH_CHANNELS}")
@@ -0,0 +1,16 @@
# RAK4631 — crypto benchmark board config
#
# UF2 output lives here rather than in prj.conf: it is nRF52-with-bootloader
# specific, and on ESP32 uf2conv fails outright (no UF2 family for that SoC).
#
# CONFIG_FLASH is enabled ONLY so the devicetree flash partitions resolve —
# uf2conv derives its base address from CONFIG_FLASH_LOAD_OFFSET, which
# USE_DT_CODE_PARTITION reads from the `zephyr,code-partition` chosen node.
# With flash off the offset comes out empty and uf2conv aborts with
# "argument -b/--base: expected one argument".
#
# This does NOT let the tool touch stored data: CONFIG_FILE_SYSTEM stays off,
# nothing mounts /lfs, and the tool never reads or writes an identity.
CONFIG_FLASH=y
CONFIG_USE_DT_CODE_PARTITION=y
CONFIG_BUILD_OUTPUT_UF2=y
@@ -0,0 +1,49 @@
/*
* RAK4631 — crypto benchmark console overlay
* SPDX-License-Identifier: MIT
*
* Minimal ON PURPOSE. The node's own board.overlay is NOT reused here: it
* declares an /fstab entry, radio, sensors and partitions, none of which this
* tool enables (CONFIG_FLASH=n, CONFIG_SPI=n ...) — pulling it in fails the
* build on an undefined lfs_partition label. Same reasoning as the LR1110
* updater's per-board overlays.
*
* RAK4631 connects USB straight to the nRF52840, so the console has to ride
* USB CDC ACM; uart0's physical pins are not reachable on a plain WisBlock
* base board.
*/
/* Same flash layout as the node build.
*
* NOT just for UF2 packaging: RAK4631 boots through the Adafruit nRF52
* bootloader, so the application must be LINKED at the bootloader's app
* offset. Without a code partition the image links at 0 and would not run even
* if uf2conv managed to package it (it does not — the missing
* CONFIG_FLASH_LOAD_OFFSET is what produces "argument -b/--base: expected one
* argument"). SoftDevice v6 layout, matching boards/nrf52840/rak4631.
*
* The delete-nodes are required by that dtsi's own contract (see its header):
* it redefines partitions the upstream board DTS already labels, and without
* removing those first the build fails with a duplicate 'storage_partition'
* label. */
/delete-node/ &boot_partition;
/delete-node/ &slot0_partition;
/delete-node/ &slot1_partition;
/delete-node/ &storage_partition;
#include "../../../../boards/common/nrf52_partitions_sdv6.dtsi"
/ {
chosen {
zephyr,code-partition = &code_partition;
zephyr,console = &cdc_acm_uart;
zephyr,shell-uart = &cdc_acm_uart;
};
};
&zephyr_udc0 {
cdc_acm_uart: cdc_acm_uart {
compatible = "zephyr,cdc-acm-uart";
};
};
+82
View File
@@ -0,0 +1,82 @@
# ZephCore crypto benchmark — minimal config.
#
# Mirrors the crypto block of boards/common/zephcore_common.conf EXACTLY, so
# what is timed is the same PSA backend selection the node builds. Any
# divergence here (a different PSA_WANT set, a different mbedTLS config) would
# change which implementation mbedTLS picks and silently invalidate the whole
# measurement.
# ========== Crypto (PSA) — copied verbatim from zephcore_common.conf ==========
CONFIG_MBEDTLS=y
CONFIG_MBEDTLS_PSA_CRYPTO_C=y
CONFIG_PSA_WANT_ALG_SHA_256=y
CONFIG_PSA_WANT_ALG_ECB_NO_PADDING=y
CONFIG_PSA_WANT_KEY_TYPE_AES=y
CONFIG_PSA_WANT_ALG_HMAC=y
CONFIG_PSA_WANT_KEY_TYPE_HMAC=y
# Portable CPU cycle counter — the measurement instrument. Same symbol the
# node enables, so enabling it here does not perturb anything relative to the
# node's own timing.
CONFIG_TIMING_FUNCTIONS=y
# sys_reboot() — Utils::cryptoPanicReboot() calls it. Kept live rather than
# stubbed: if a benchmarked primitive ever panics, the tool should reboot the
# way the node would, not quietly print a number that means nothing.
CONFIG_REBOOT=y
# C++ — Utils, Identity and Packet are C++
CONFIG_CPP=y
CONFIG_STD_CPP17=y
CONFIG_REQUIRES_FULL_LIBC=y
# Console only, no log subsystem. All output is printk, which is synchronous —
# important here, because an asynchronous log backend would do work between
# timed regions and inflate the mean.
CONFIG_SERIAL=y
CONFIG_CONSOLE=y
CONFIG_UART_CONSOLE=y
CONFIG_PRINTK=y
CONFIG_LOG=n
# No floating-point printf: every figure this tool prints is integer math on
# purpose (see print_result / airtime_us). Leaving FP support off keeps the
# image small and rules out an FP-emulation call inside a timed region.
CONFIG_CBPRINTF_FP_SUPPORT=n
# CONFIG_ASSERT stays off: on ESP32 the Espressif blob trips false kswap.h
# assertions, and this tool has no reason to differ from the node.
CONFIG_ASSERT=n
# Main stack: Ed25519 verify through Monocypher plus the static test vectors
# and the ~200-byte scratch buffers in Utils::encrypt. 4096 is generous; the
# node runs 8192.
CONFIG_MAIN_STACK_SIZE=4096
# NOTE: no CONFIG_BUILD_OUTPUT_UF2 here — it is nRF52-with-bootloader
# specific and fails outright on ESP32. It lives in boards/rak4631/board.conf.
# USB CDC ACM console — nRF52840 boards (RAK4631) route console over USB
# because the UART pins are not reachable on a plain base board. Harmless on
# ESP32, which uses its own console and ignores these.
CONFIG_USB_DEVICE_STACK_NEXT=y
CONFIG_UART_LINE_CTRL=y
CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT=y
CONFIG_CDC_ACM_SERIAL_ENABLE_AT_BOOT=y
# Explicitly off — this tool must not touch the radio, storage or BLE. In
# particular it must never mount /lfs: it does not read or write any stored
# identity, so a device under test keeps whatever it already had.
#
# Turning BT off matters for a second reason on nRF: CONFIG_CRYPTO_NRF_ECB
# depends on !HAS_BT_CTLR. If anyone later extends this tool to compare a
# hardware AES backend, that comparison is only valid against a build with the
# same BT setting the target role actually uses — see
# HANDOVER_crypto_hw_accel.md, "Traps and constraints".
CONFIG_BT=n
CONFIG_FLASH=n
CONFIG_FILE_SYSTEM=n
CONFIG_SPI=n
CONFIG_I2C=n
CONFIG_SENSOR=n
CONFIG_GNSS=n
+524
View File
@@ -0,0 +1,524 @@
/*
* ZephCore crypto benchmark
* SPDX-License-Identifier: MIT
*
* Answers one question: does ZephCore's crypto cost enough to be worth
* accelerating? See HANDOVER_crypto_hw_accel.md every option in that
* document (a second Zephyr-crypto-API backend in Utils.cpp, nrfxlib PSA
* drivers in west.yml, or the free software wins) is gated on numbers that
* nobody has ever taken.
*
* WHY A SEPARATE IMAGE rather than a CLI command on a live node:
* the measurement has to be free of mesh-loop, BLE and LoRa-RX interference,
* and it runs each primitive hundreds of times back to back which would
* starve exactly those subsystems. Same reasoning as tools/rng_selftest.
*
* WHAT IT MEASURES, and why the answer is a RATIO not a number:
* the absolute cost of SHA-256 is uninteresting on its own. What decides the
* question is crypto time as a fraction of packet AIRTIME at SF11/250 kHz a
* packet occupies the channel for hundreds of milliseconds, so a per-packet
* crypto bill in the tens of microseconds is noise, and the whole hardware
* exploration closes with a documented "no". The final table does that
* division for you, per LoRa preset, using the SAME airtime formula the node
* uses (LoRaRadioBase::getAirtimeMillis) so the comparison is apples to
* apples.
*
* It measures the SHIPPING code: src/Utils.cpp, src/Identity.cpp and
* src/Packet.cpp are compiled straight from the main tree, not copied. What
* is timed here is byte-for-byte what the node runs.
*
* The tool never reads or writes stored identity, and never touches the
* radio, flash or BLE. A device under test keeps whatever key it had.
*/
#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
#include <zephyr/timing/timing.h>
#include <psa/crypto.h>
#include <string.h>
#include <mesh/Utils.h>
#include <mesh/Identity.h>
#include <mesh/Packet.h>
using namespace mesh;
#ifndef CRYPTO_BENCH_ITERS
#define CRYPTO_BENCH_ITERS 200
#endif
/* Ed25519 is ~3 orders of magnitude slower than the symmetric primitives.
* At the default iteration count a full sweep would take minutes and tell us
* nothing extra the variance on an operation that long is negligible. */
#ifndef CRYPTO_BENCH_ITERS_SLOW
#define CRYPTO_BENCH_ITERS_SLOW 20
#endif
/* Payload sizes. 184 is MAX_PACKET_PAYLOAD; calculatePacketHash hashes
* 1 + payload_len, so the hash input tops out at 185. 60 is a typical text
* message, 100 a typical advert. */
#define PAYLOAD_TYPICAL 60
#define PAYLOAD_ADVERT 100
#define PAYLOAD_MAX MAX_PACKET_PAYLOAD
/* Worst case for the group-channel loop at Mesh.cpp:398. */
#ifndef CRYPTO_BENCH_CHANNELS
#define CRYPTO_BENCH_CHANNELS 40
#endif
/* Anti-elision. Without a visible consumer the optimiser is entitled to
* delete calls whose output is unused, and at -Os it does.
*
* It reads only the first and last byte, NOT the whole buffer: the sink runs
* INSIDE the timed region, and summing 176 bytes would add real work to every
* AES measurement. Two bytes is enough the compiler cannot prove the rest
* of the buffer was unnecessary to produce them. */
static volatile uint32_t g_sink;
static inline void sink(const uint8_t *p, size_t n)
{
g_sink += (uint32_t)p[0] + (uint32_t)p[n - 1];
}
/* ---------------------------------------------------------------- timing */
/* Zephyr's portable cycle counter (DWT on Cortex-M, CCOUNT on Xtensa) — the
* same mechanism ZephyrRNG's two-clock beat uses. See
* memory/zephyrrng-entropy.md for the platform traps.
*
* MIN, not mean, is the headline figure: every perturbation (interrupt,
* cache miss, flash wait state) can only ADD time, so the minimum over N runs
* is the closest estimate of the true cost. Mean is printed alongside because
* a large min/mean gap is itself information it says the operation is being
* interfered with, which matters on a node where it shares a CPU with the
* radio ISR. */
struct BenchResult {
const char *name;
uint64_t min_ns;
uint64_t mean_ns;
int iters;
};
static uint64_t cycles_to_ns(uint64_t cycles)
{
return timing_cycles_to_ns(cycles);
}
/* Parameter names are underscore-prefixed on purpose: the preprocessor
* substitutes macro arguments after `.` too, so a parameter called `iters`
* would rewrite the `(dst).iters` member access into `(dst).200`. */
#define BENCH(dst, _label, _n, stmt) \
do { \
uint64_t _min = UINT64_MAX, _sum = 0; \
for (int _i = 0; _i < (_n); _i++) { \
timing_t _t0 = timing_counter_get(); \
{ stmt; } \
timing_t _t1 = timing_counter_get(); \
uint64_t _c = timing_cycles_get(&_t0, &_t1); \
if (_c < _min) _min = _c; \
_sum += _c; \
} \
(dst).name = (_label); \
(dst).min_ns = cycles_to_ns(_min); \
(dst).mean_ns = cycles_to_ns(_sum / (uint64_t)(_n)); \
(dst).iters = (_n); \
} while (0)
/* us with 3 decimals, integer math — no CBPRINTF_FP_SUPPORT needed. */
static void print_result(const BenchResult *r)
{
uint64_t mn = r->min_ns, mu = r->mean_ns;
printk(" %-34s %6llu.%03llu %8llu.%03llu %4d\n",
r->name, mn / 1000u, mn % 1000u, mu / 1000u, mu % 1000u, r->iters);
}
static void print_header(const char *section)
{
printk("\n%s\n", section);
printk(" %-34s %10s %12s %4s\n", "operation", "min us", "mean us", "N");
printk(" ---------------------------------- ---------- ------------ ----\n");
}
/* --------------------------------------------------------------- airtime */
/* Integer port of LoRaRadioBase::getAirtimeMillis(), in microseconds.
* Deliberately mirrors that function line for line including the
* preamble-length rule and the LDRO threshold tied to symbol time (not to
* SF), because a divergence here would silently distort the only comparison
* this tool exists to make. */
static uint32_t preamble_syms(uint8_t sf)
{
return (sf <= 8) ? 32u : 16u; /* PR #1954 parity */
}
static uint64_t airtime_us(uint8_t sf, uint32_t bw_hz, uint8_t cr, uint32_t len)
{
/* Tsym in ns, to keep the LDRO comparison exact at narrow BW. */
uint64_t tsym_ns = ((uint64_t)(1u << sf) * 1000000000ull) / bw_hz;
/* 4.25 preamble symbols -> multiply by 425, divide by 100. */
uint64_t t_pre_ns = ((uint64_t)preamble_syms(sf) * 100ull + 425ull) * tsym_ns / 100ull;
int de = (tsym_ns > 16380000ull) ? 1 : 0; /* > 16.38 ms */
int64_t num = 8ll * (int64_t)len - 4ll * sf + 28 + 16;
int64_t den = 4ll * ((int64_t)sf - 2ll * de);
if (den < 1) den = 4;
int64_t ceil_div = (num + den - 1) / den;
int64_t n_pay = 8 + (ceil_div * (int64_t)cr > 0 ? ceil_div * (int64_t)cr : 0);
return (t_pre_ns + (uint64_t)n_pay * tsym_ns) / 1000ull;
}
struct Preset {
const char *name;
uint8_t sf;
uint32_t bw_hz;
uint8_t cr; /* 5..8, used as (cr-4+4) == cr in the formula */
};
static const Preset presets[] = {
{ "SF7 BW62.5k CR4/5", 7, 62500, 5 }, /* EmpireMesh, since 2026-07-25 */
{ "SF8 BW62.5k CR4/8", 8, 62500, 8 }, /* EmpireMesh, before that */
{ "SF10 BW125k CR4/5", 10, 125000, 5 },
{ "SF11 BW250k CR4/5", 11, 250000, 5 }, /* MeshCore default */
};
/* ------------------------------------------------------------------ main */
int main(void)
{
k_msleep(2000); /* USB CDC enumerates after boot on some boards */
timing_init();
timing_start();
/* Utils.cpp assumes PSA is initialised. On a node that happens inside
* ZephyrRNG (adapters/rng/ZephyrRNG.cpp:285), which this tool
* deliberately does not link so do it here, and fail loudly rather
* than silently benchmarking a pile of PSA_ERROR returns. */
psa_status_t ps = psa_crypto_init();
if (ps != PSA_SUCCESS) {
printk("FATAL: psa_crypto_init failed: %d\n", (int)ps);
return 0;
}
printk("\n");
printk("========================================================\n");
printk(" ZephCore crypto benchmark\n");
printk(" board: %s N=%d (slow ops N=%d)\n",
CONFIG_BOARD, CRYPTO_BENCH_ITERS, CRYPTO_BENCH_ITERS_SLOW);
printk("========================================================\n");
/* Test vectors. Fixed, not random: this is a timing measurement and a
* deterministic input makes runs comparable across boards and builds.
* Ed25519/X25519 timing is data-dependent at the margins, which the
* min-over-N already absorbs. */
static uint8_t buf[1 + MAX_PACKET_PAYLOAD + CIPHER_BLOCK_SIZE];
static uint8_t out[MAX_PACKET_PAYLOAD + CIPHER_BLOCK_SIZE * 2];
static uint8_t key[PUB_KEY_SIZE];
static uint8_t hash[MAX_HASH_SIZE];
static uint8_t sig[SIGNATURE_SIZE];
static uint8_t secret[CIPHER_KEY_SIZE * 2];
for (size_t i = 0; i < sizeof(buf); i++) buf[i] = (uint8_t)(i * 7 + 3);
for (size_t i = 0; i < sizeof(key); i++) key[i] = (uint8_t)(i * 11 + 5);
uint8_t seed[SEED_SIZE];
for (size_t i = 0; i < sizeof(seed); i++) seed[i] = (uint8_t)(i * 13 + 1);
LocalIdentity self;
self.fromSeed(seed);
uint8_t seed2[SEED_SIZE];
for (size_t i = 0; i < sizeof(seed2); i++) seed2[i] = (uint8_t)(i * 17 + 9);
LocalIdentity peer;
peer.fromSeed(seed2);
self.sign(sig, buf, PAYLOAD_ADVERT);
if (!self.verify(sig, buf, PAYLOAD_ADVERT)) {
printk("FATAL: sign/verify self-check failed — results would be "
"meaningless\n");
return 0;
}
BenchResult r;
/* ---- instrument baseline ---------------------------------------- */
print_header("Instrument baseline — subtract this from everything below");
/* The cost of the two timing_counter_get() calls and the sink, with no
* crypto between them. Every figure in this run carries it. If it is
* not small compared to the fastest operation measured, the fastest
* rows are measuring the ruler, not the thing. */
BENCH(r, "empty (timer pair + sink)", CRYPTO_BENCH_ITERS,
sink(buf, 16));
print_result(&r);
uint64_t overhead = r.min_ns;
/* ---- primitives: SHA-256 ---------------------------------------- */
print_header("SHA-256 (PSA one-shot) — the dedup hash");
BENCH(r, "sha256, 16 B", CRYPTO_BENCH_ITERS,
Utils::sha256(hash, MAX_HASH_SIZE, buf, 16); sink(hash, MAX_HASH_SIZE));
print_result(&r);
uint64_t sha_16 = r.min_ns;
BENCH(r, "sha256, 61 B (typical msg pkt)", CRYPTO_BENCH_ITERS,
Utils::sha256(hash, MAX_HASH_SIZE, buf, 1 + PAYLOAD_TYPICAL); sink(hash, MAX_HASH_SIZE));
print_result(&r);
uint64_t sha_typical = r.min_ns;
BENCH(r, "sha256, 101 B (typical advert)", CRYPTO_BENCH_ITERS,
Utils::sha256(hash, MAX_HASH_SIZE, buf, 1 + PAYLOAD_ADVERT); sink(hash, MAX_HASH_SIZE));
print_result(&r);
BENCH(r, "sha256, 185 B (max payload)", CRYPTO_BENCH_ITERS,
Utils::sha256(hash, MAX_HASH_SIZE, buf, 1 + PAYLOAD_MAX); sink(hash, MAX_HASH_SIZE));
print_result(&r);
uint64_t sha_max = r.min_ns;
BENCH(r, "sha256, 2-frag (32+32 B)", CRYPTO_BENCH_ITERS,
Utils::sha256(hash, MAX_HASH_SIZE, buf, 32, buf + 32, 32); sink(hash, MAX_HASH_SIZE));
print_result(&r);
/* ---- primitives: AES ------------------------------------------- */
print_header("AES-128-ECB (PSA, key imported+destroyed per call)");
BENCH(r, "encrypt, 16 B (1 block)", CRYPTO_BENCH_ITERS,
int n = Utils::encrypt(key, out, buf, 16); sink(out, n > 0 ? n : 1));
print_result(&r);
uint64_t aes_1blk = r.min_ns;
BENCH(r, "encrypt, 60 B (4 blocks)", CRYPTO_BENCH_ITERS,
int n = Utils::encrypt(key, out, buf, PAYLOAD_TYPICAL); sink(out, n > 0 ? n : 1));
print_result(&r);
BENCH(r, "encrypt, 176 B (11 blocks)", CRYPTO_BENCH_ITERS,
int n = Utils::encrypt(key, out, buf, 176); sink(out, n > 0 ? n : 1));
print_result(&r);
uint64_t aes_max = r.min_ns;
/* decrypt needs valid ciphertext-shaped input; ECB accepts anything
* block-aligned, and the timing does not depend on the plaintext. */
BENCH(r, "decrypt, 176 B (11 blocks)", CRYPTO_BENCH_ITERS,
int n = Utils::decrypt(key, out, buf, 176); sink(out, n > 0 ? n : 1));
print_result(&r);
/* ---- the per-call key import ------------------------------------ */
print_header("PSA key handling — the per-call import/destroy overhead");
/* Isolates what src/Utils.cpp:86,101 and :149,159 spend on key
* lifecycle ALONE, with no cipher or MAC work attached. This is the
* number that decides whether caching a key handle per channel secret
* is worth changing Utils.cpp for: compare it against "AES encrypt,
* 16 B" above. If import+destroy dominates a one-block encrypt, the
* group-channel loop is paying for key management, not crypto. */
BENCH(r, "psa_import_key + psa_destroy_key", CRYPTO_BENCH_ITERS, {
psa_key_attributes_t attr = PSA_KEY_ATTRIBUTES_INIT;
psa_set_key_type(&attr, PSA_KEY_TYPE_AES);
psa_set_key_bits(&attr, CIPHER_KEY_SIZE * 8);
psa_set_key_usage_flags(&attr, PSA_KEY_USAGE_ENCRYPT);
psa_set_key_algorithm(&attr, PSA_ALG_ECB_NO_PADDING);
psa_key_id_t kid;
if (psa_import_key(&attr, key, CIPHER_KEY_SIZE, &kid) == PSA_SUCCESS) {
g_sink += (uint32_t)kid;
psa_destroy_key(kid);
}
});
print_result(&r);
uint64_t key_import = r.min_ns;
/* ---- HMAC, by subtraction --------------------------------------- */
print_header("HMAC-SHA256 (via encryptThenMAC / MACThenDecrypt)");
/* compute_hmac_truncated() is static in Utils.cpp, so it cannot be
* called directly and is NOT worth un-staticing for a benchmark. It is
* derived instead: encryptThenMAC = encrypt + HMAC, and the failing
* MACThenDecrypt path is HMAC + a constant-time compare and nothing
* else (it returns before decrypt on MAC mismatch) so the mismatch
* case below is very nearly a direct HMAC measurement. */
BENCH(r, "encryptThenMAC, 60 B (TX path)", CRYPTO_BENCH_ITERS,
int n = Utils::encryptThenMAC(key, out, buf, PAYLOAD_TYPICAL); sink(out, n > 0 ? n : 1));
print_result(&r);
uint64_t etm_typical = r.min_ns;
BENCH(r, "encryptThenMAC, 176 B (TX path)", CRYPTO_BENCH_ITERS,
int n = Utils::encryptThenMAC(key, out, buf, 176); sink(out, n > 0 ? n : 1));
print_result(&r);
/* A real ciphertext, so the success path actually reaches decrypt. */
static uint8_t ct[MAX_PACKET_PAYLOAD + CIPHER_BLOCK_SIZE * 2];
int ct_len = Utils::encryptThenMAC(key, ct, buf, PAYLOAD_TYPICAL);
if (ct_len <= 0) {
printk("FATAL: encryptThenMAC produced nothing\n");
return 0;
}
BENCH(r, "MACThenDecrypt, MATCH (RX ours)", CRYPTO_BENCH_ITERS,
int n = Utils::MACThenDecrypt(key, out, ct, ct_len); sink(out, n > 0 ? n : 1));
print_result(&r);
uint64_t mtd_match = r.min_ns;
/* Wrong key -> MAC mismatch -> returns before AES. This is the cost of
* ONE non-matching channel in the Mesh.cpp:398 loop. */
static uint8_t wrong_key[PUB_KEY_SIZE];
for (size_t i = 0; i < sizeof(wrong_key); i++) wrong_key[i] = (uint8_t)(i * 3 + 200);
BENCH(r, "MACThenDecrypt, MISS (wrong chan)", CRYPTO_BENCH_ITERS,
int n = Utils::MACThenDecrypt(wrong_key, out, ct, ct_len); g_sink += (uint32_t)n);
print_result(&r);
uint64_t mtd_miss = r.min_ns;
/* ---- asymmetric -------------------------------------------------- */
print_header("Ed25519 / X25519 (Monocypher, software only)");
BENCH(r, "Ed25519 verify (advert RX)", CRYPTO_BENCH_ITERS_SLOW,
bool ok = self.verify(sig, buf, PAYLOAD_ADVERT); g_sink += ok ? 1 : 0);
print_result(&r);
uint64_t ed_verify = r.min_ns;
BENCH(r, "Ed25519 sign (advert TX)", CRYPTO_BENCH_ITERS_SLOW,
self.sign(sig, buf, PAYLOAD_ADVERT); sink(sig, SIGNATURE_SIZE));
print_result(&r);
BENCH(r, "X25519 shared secret (contact)", CRYPTO_BENCH_ITERS_SLOW,
self.calcSharedSecret(secret, peer); sink(secret, sizeof(secret)));
print_result(&r);
/* ---- composites: real call paths -------------------------------- */
print_header("Per-packet totals — the real RX/TX call paths");
/* Dedup as implemented today. SimpleMeshTables::wasSeen() and
* markSeen() each call packet->calculatePacketHash() independently,
* and since the 2026-07-02 hasSeen split every caller invokes markSeen
* right after wasSeen returns false so a NEW packet hashes twice.
* The linear memcmp scan those functions also do is not crypto and is
* not timed here; it is a 160-entry 8-byte compare, nowhere near the
* hash. Packet.cpp is compiled from the main tree, so the buffer
* assembly (the memcpy into a stack buffer) is included, as it is on a
* node. */
/* Not `static`: a function-local static with a non-trivial constructor
* emits __cxa_guard_acquire/release, which Zephyr's minimal C++ runtime
* does not provide. ~260 bytes on the stack instead. */
Packet pkt;
pkt.header = (PAYLOAD_TYPE_TXT_MSG << PH_TYPE_SHIFT) | ROUTE_TYPE_FLOOD;
pkt.payload_len = PAYLOAD_TYPICAL;
memcpy(pkt.payload, buf, PAYLOAD_TYPICAL);
BENCH(r, "dedup, 1x calculatePacketHash", CRYPTO_BENCH_ITERS,
pkt.calculatePacketHash(hash); sink(hash, MAX_HASH_SIZE));
print_result(&r);
uint64_t dedup_1 = r.min_ns;
BENCH(r, "dedup, 2x (wasSeen + markSeen)", CRYPTO_BENCH_ITERS,
pkt.calculatePacketHash(hash); sink(hash, MAX_HASH_SIZE);
pkt.calculatePacketHash(hash); sink(hash, MAX_HASH_SIZE));
print_result(&r);
uint64_t dedup_2 = r.min_ns;
/* Group packet that matches no configured channel: the loop runs every
* channel and every one costs a full HMAC. */
BENCH(r, "group RX, " STRINGIFY(CRYPTO_BENCH_CHANNELS) " channel misses",
CRYPTO_BENCH_ITERS / 4,
for (int c = 0; c < CRYPTO_BENCH_CHANNELS; c++) {
int n = Utils::MACThenDecrypt(wrong_key, out, ct, ct_len);
g_sink += (uint32_t)n;
});
print_result(&r);
uint64_t group_miss_all = r.min_ns;
/* ---- the verdict table ------------------------------------------ */
printk("\n");
printk("========================================================\n");
printk(" PER-PACKET CRYPTO BUDGET vs AIRTIME\n");
printk("--------------------------------------------------------\n");
struct Scenario {
const char *name;
uint64_t ns;
uint32_t pkt_bytes;
};
/* Packet byte counts are the on-air length the airtime formula wants:
* 2 header bytes + path + payload. Path is assumed 3 hops x 1-byte
* hash, a common flood. */
const uint32_t hdr_path = 2 + 3;
const Scenario scen[] = {
{ "flood not for us (dedup only)",
dedup_2, hdr_path + PAYLOAD_TYPICAL },
{ "direct msg for us (dedup+MAC+AES)",
dedup_2 + mtd_match, hdr_path + PAYLOAD_TYPICAL },
{ "group msg, no channel matches",
dedup_2 + group_miss_all, hdr_path + PAYLOAD_TYPICAL },
{ "advert (dedup + Ed25519 verify)",
dedup_2 + ed_verify, hdr_path + PAYLOAD_ADVERT },
{ "our TX (encryptThenMAC + dedup)",
dedup_1 + etm_typical, hdr_path + PAYLOAD_TYPICAL },
};
for (size_t p = 0; p < ARRAY_SIZE(presets); p++) {
printk("\n %s\n", presets[p].name);
printk(" %-36s %9s %9s %8s\n",
"scenario", "crypto us", "air us", "share");
printk(" ------------------------------------ --------- --------- --------\n");
for (size_t s = 0; s < ARRAY_SIZE(scen); s++) {
uint64_t air = airtime_us(presets[p].sf, presets[p].bw_hz,
presets[p].cr, scen[s].pkt_bytes);
uint64_t cry_us = scen[s].ns / 1000u;
/* share in hundredths of a percent (basis points) */
uint64_t bp = air ? (scen[s].ns * 10000ull) / (air * 1000ull) : 0;
printk(" %-36s %9llu %9llu %3llu.%02llu%%\n",
scen[s].name, cry_us, air, bp / 100u, bp % 100u);
}
}
/* ---- what the software wins would buy --------------------------- */
printk("\n");
printk("--------------------------------------------------------\n");
printk(" HEADROOM OF THE FREE SOFTWARE WINS (no hardware needed)\n");
printk("--------------------------------------------------------\n");
printk(" single-hash dedup saves : %llu us/packet\n",
(dedup_2 - dedup_1) / 1000u);
printk(" cached key handle would save up to: %llu us/packet\n",
((uint64_t)CRYPTO_BENCH_CHANNELS * key_import) / 1000u);
printk(" (%d channels x %llu.%03llu us import/destroy)\n",
CRYPTO_BENCH_CHANNELS, key_import / 1000u, key_import % 1000u);
printk(" key import as share of a 1-block AES: %llu%%\n",
aes_1blk ? (key_import * 100u) / aes_1blk : 0);
printk("\n");
printk("--------------------------------------------------------\n");
printk(" HOW TO READ THIS\n");
printk("--------------------------------------------------------\n");
printk(" The 'share' column is the whole question. Crypto competes\n");
printk(" with airtime, not with itself. If every scenario sits well\n");
printk(" under 1%%, hardware acceleration cannot buy throughput the\n");
printk(" radio would ever notice, and the honest answer to\n");
printk(" HANDOVER_crypto_hw_accel.md is Option C (do nothing) --\n");
printk(" regardless of how large the microsecond figures look.\n");
printk(" Only a scenario in the double-digit percent range, or one\n");
printk(" whose absolute cost approaches the inter-packet gap,\n");
printk(" justifies going further.\n");
printk(" Power is NOT measured here. A CPU-bound millisecond also\n");
printk(" costs battery; if the share numbers are borderline, measure\n");
printk(" current draw before deciding.\n");
printk("\n");
printk("========================================================\n");
printk(" reference: sha16=%llu.%03llu us sha185=%llu.%03llu us\n",
sha_16 / 1000u, sha_16 % 1000u, sha_max / 1000u, sha_max % 1000u);
printk(" reference: aes1blk=%llu.%03llu us aes176=%llu.%03llu us\n",
aes_1blk / 1000u, aes_1blk % 1000u, aes_max / 1000u, aes_max % 1000u);
printk(" reference: hmac_miss=%llu.%03llu us ed_verify=%llu.%03llu us\n",
mtd_miss / 1000u, mtd_miss % 1000u, ed_verify / 1000u, ed_verify % 1000u);
printk(" instrument overhead=%llu ns (already included in every row)\n",
overhead);
printk(" (sha_typical=%llu ns, sink=%u — ignore, anti-elision)\n",
sha_typical, (unsigned)g_sink);
printk("========================================================\n");
printk("\n[crypto_bench] done — halting. Reflash normal firmware.\n");
timing_stop();
return 0;
}