mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-02 00:38:50 +00:00
gps fiddling
This commit is contained in:
@@ -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/
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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`. |
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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)
|
||||
@@ -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";
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user