6.2 KiB
Firmware Review — blackpillrtkcode.ino (v1 → v2)
Review of the as-deployed watchdog firmware, with the fixes shipped in
04-Source-Code/firmware/blackpillrtkcode_v2/blackpillrtkcode_v2.ino. Severity: 🔴 likely field-impacting ·
🟠 robustness · 🟡 cleanup.
UBX command checksums and ADC divider math were verified programmatically (see end of this doc) — those parts of v1 are correct.
Issues found
🔴 1. Loss-of-corrections recovery targets the wrong subsystem
v1: after SOFT_RESET_DELAY (5 min) of fixQuality < 4, it sends
UBX_SOFT_RESET (a GNSS hot-reset) to the ZED-F9P.
Problem: corrections arrive over the 4G NTRIP modem, not from the GNSS.
Hot-resetting the receiver does nothing for a hung modem — the dominant field
failure per the client. Five minutes are spent on a no-op before the only
effective action (a full power-cycle at 10 min).
v2: drops the GNSS hot-reset; recovery = power-cycle the whole stack
(which restarts the 4G module), gated by a grace window and backoff.
🔴 2. Hard-reset has no backoff → reboot-loop / Wialon flapping
v1: after HARD_RESET_DELAY (10 min) it power-cycles, returns to BROWNOUT,
immediately powers back on, and — if the fault persists — does it again every
~10 min indefinitely. Each cycle is a multi-minute disconnect.
Problem: a genuinely dead 4G card, or a unit parked without sky view, makes
the unit flap off Wialon forever. This is a prime suspect for "desconexiones
recurrentes."
v2: exponential backoff (1→2→4→8→16 min, capped 20 min) then a latched
LIMP mode that keeps the unit reporting standalone GPS and only retries
recovery every 30 min. The unit never silently vanishes.
🔴 3. No detection of a fully hung stack (NMEA silence)
v1: only reacts via fixQuality. If the GNSS/4G stack hangs and stops
emitting NMEA entirely, fixQuality simply freezes at its last value — the
watchdog may not act at all, or acts on stale data.
v2: tracks lastNmeaMs; no NMEA for 60 s while powered ⇒ power-cycle now.
Catches hung hardware far faster than the fix-quality ladder.
🟠 4. No hysteresis / debounce on vehicle-power & ignition
v1: pa4_on = (v5vF > 4.0) is a single hard threshold; ignition is a raw
digitalRead. Around the threshold (alternator noise, ignition flicker), the
rate mode (1 Hz↔30 s) and state can chatter.
v2: hysteresis band (ON >4.2 V, OFF <3.8 V) + 750 ms debounce on both
vehicle-power and ignition.
🟠 5. Brownout exit has no debounce (oscillation risk)
v1: entry to BROWNOUT is debounced 3 s, but the exit powers on the instant
pa4_on || pa5_on || vBat>3.7. A brief blip near the threshold can start a
power-on it can't sustain → drain → repeat.
v2: wake is driven by the same debounced/hysteretic signals; brownout entry
uses a 5 s confirmation.
🟠 6. No thermal awareness (the actual field failure mode)
v1: nothing measures temperature, despite "died from heat."
v2: reads the STM32 internal temp sensor (F411 factory cal), logs session
min/max, derates to 30 s above 75 °C to cut self-heat/power, and shows it
on the dashboard. Firmware can't cool the box, but a pulled unit's tMax now
proves or kills the thermal hypothesis. (Disable with ENABLE_MCU_THERMAL 0
if your core lacks ATEMP/AVREF.)
🟡 7. String heap churn on an ARM MCU
v1: comments claim const char* avoids fragmentation, but nmeaBuffer is a
String, and drawDashboard builds a String paddedMsg every refresh.
v2: fixed char nmeaBuf[110] parser (no heap) and snprintf padding.
🟡 8. Doc/firmware mismatch: boot delay
The LED table says "waiting 25 s"; v1 BOOT_STABILIZE_TIME is 15 s.
v2: set to 25 s to match the documented SOP (gives the 4G modem more time
to attach before the supervisor starts judging the link). Tune on the bench.
🟡 9. Minor: redundant pinMode before GNSS.begin()
Harmless (the serial peripheral reconfigures the pins); left as explicit intent in v2.
What v1 got right (kept in v2)
- Float is not treated as failure.
fixQuality >= 4correctly counts both RTK fixed (4) and float (5) as healthy. (Earlier verbal claim that FLOAT triggered resets was wrong — corrected here.) - Floating the TX pin on power-off to avoid back-powering the rail through a GPIO.
- Low-pass filtering the ADC readings.
- Hysteresis between
BAT_CRITICAL(3.4 V) andBAT_RECOVERY(3.7 V). - The UBX rate commands and their checksums (verified below).
v2 recovery state machine (summary)
SLEEP ──(vehicle pwr | ignition | Vbat>3.7)──► BOOTING ──(25 s)──► RUNNING
▲ │
│ Vbat<3.4 & no pwr & no ign (5 s) │
└───────────────────────────────────────────────────────── RUNNING supervises:
• no NMEA 60 s ........................► POWER_CYCLE (gated by backoff)
• RTK lost > 120 s grace .............► POWER_CYCLE (gated by backoff)
• RTK healthy 5 min .................► clear backoff + LIMP latch
POWER_CYCLE (6 s rail off) ──► BOOTING ; after 5 fails ──► LIMP (keep reporting)
Verification performed (2026-06-19)
- UBX checksums recomputed (8-bit Fletcher over class…payload):
UBX_1HZ→01 39✓,UBX_30S→1E AD✓,UBX_SOFT_RESET→0F 66✓. - CFG-RATE decode: 1 Hz = measRate 1000 ms × navRate 1; 30 s = 1000 ms × 30 ✓.
- ADC dividers (12-bit, 3.3 V, ÷2): LiPo 3.4–4.2 V → 1.70–2.10 V at pin (2110–2606 counts); 5 V rail → 2.50 V (3102 counts). No ADC clipping ✓.
- Backoff ladder: 1, 2, 4, 8, 16 min → LIMP @ 30 min, as designed ✓.
- Compile: v2 builds clean under
g++ -std=c++14 -Wall -Wextraagainst a hardware stub (logic/types/flow only).
⚠️ The stub compile validates logic, not the STM32 target. Bench-flash and test v2 on real hardware (Arduino IDE / PlatformIO, STM32duino core) before any field deployment — especially the thermal read and the power-cycle timing.