Drogheda/03-Architecture-Hardware/firmware-review.md

6.2 KiB
Raw Blame History

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 >= 4 correctly 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) and BAT_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 -Wextra against 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.